From 58dbcd71689d987caa783ae251fcd26db44a41de Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 25 Nov 2023 14:13:55 +0100 Subject: [PATCH 001/300] major refactor to flash code --- sed/loader/fel/__init__.py | 14 + sed/loader/fel/buffer.py | 172 +++++++++ sed/loader/fel/dataframe.py | 395 +++++++++++++++++++ sed/loader/fel/multiindex.py | 97 +++++ sed/loader/fel/parquet.py | 83 ++++ sed/loader/flash/loader.py | 717 ++--------------------------------- 6 files changed, 792 insertions(+), 686 deletions(-) create mode 100644 sed/loader/fel/__init__.py create mode 100644 sed/loader/fel/buffer.py create mode 100644 sed/loader/fel/dataframe.py create mode 100644 sed/loader/fel/multiindex.py create mode 100644 sed/loader/fel/parquet.py diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py new file mode 100644 index 00000000..aaf15800 --- /dev/null +++ b/sed/loader/fel/__init__.py @@ -0,0 +1,14 @@ +"""sed.loader.fel module easy access APIs + +""" +from .buffer import BufferFileHandler +from .dataframe import DataFrameCreator +from .multiindex import MultiIndexCreator +from .parquet import ParquetHandler + +__all__ = [ + "BufferFileHandler", + "DataFrameCreator", + "MultiIndexCreator", + "ParquetHandler", +] diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py new file mode 100644 index 00000000..f94094c8 --- /dev/null +++ b/sed/loader/fel/buffer.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TypeVar + +import dask.dataframe as ddf +import numpy as np +import pyarrow.parquet as pq +from joblib import delayed +from joblib import Parallel +from pandas import DataFrame + +from sed.core.dfops import forward_fill_lazy +from sed.loader.fel.dataframe import DataFrameCreator +from sed.loader.fel.parquet import ParquetHandler + +DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) + + +class BufferFileHandler(ParquetHandler, DataFrameCreator): + def __init__( + self, + config_dataframe: dict, + h5_paths: list[Path], + folder: Path, + force_recreate: bool, + prefix: str = "", + suffix: str = "", + ): + if len(h5_paths): + raise ValueError("No data available. Probably failed reading all h5 files") + + h5_filenames = [Path(h5_path).stem for h5_path in h5_paths] + super().__init__(config_dataframe) + super().__init__(h5_filenames, folder, "buffer", prefix, suffix) + + if not force_recreate: + self.schema_check() + + self.parallel_buffer_file_creation(h5_paths, force_recreate) + + self.fill() + + def schema_check(self) -> None: + """ + Checks the schema of the Parquet files. + + Raises: + ValueError: If the schema of the Parquet files do not match the configuration. + """ + existing_parquet_filenames = [file for file in self.parquet_paths if file.exists()] + # Check if the available channels match the schema of the existing parquet files + parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] + config_schema = set(self.get_channels(formats="all", index=True)) + if self._config.get("split_sector_id_from_dld_time", False): + config_schema.add(self._config.get("sector_id_column", False)) + + for i, schema in enumerate(parquet_schemas): + schema_set = set(schema.names) + if schema_set != config_schema: + missing_in_parquet = config_schema - schema_set + missing_in_config = schema_set - config_schema + + missing_in_parquet_str = ( + f"Missing in parquet: {missing_in_parquet}" if missing_in_parquet else "" + ) + missing_in_config_str = ( + f"Missing in config: {missing_in_config}" if missing_in_config else "" + ) + + raise ValueError( + "The available channels do not match the schema of file", + f"{existing_parquet_filenames[i]}", + f"{missing_in_parquet_str}", + f"{missing_in_config_str}", + "Please check the configuration file or set force_recreate to True.", + ) + + def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> bool | Exception: + """ + Converts an HDF5 file to Parquet format to create a buffer file. + + This method uses `create_dataframe_per_file` method to create dataframes from individual + files within an HDF5 file. The resulting dataframe is then saved to a Parquet file. + + Args: + h5_path (Path): Path to the input HDF5 file. + parquet_path (Path): Path to the output Parquet file. + + Raises: + ValueError: If an error occurs during the conversion process. + + """ + try: + ( + self.create_dataframe_per_file(h5_path) + .reset_index(level=self.multi_index) + .to_parquet(parquet_path, index=False) + ) + except Exception as exc: # pylint: disable=broad-except + self.failed_files_error.append(f"{parquet_path}: {type(exc)} {exc}") + return exc + return None + + def parallel_buffer_file_creation(self, h5_paths: list[Path], force_recreate) -> None: + """ + Parallelizes the creation of buffer files. + + Args: + h5_paths (List[Path]): List of paths to the input HDF5 files. + + Raises: + ValueError: If an error occurs during the conversion process. + + """ + files_to_read = [ + (h5_path, parquet_path) + for h5_path, parquet_path in zip(h5_paths, self.parquet_paths) + if force_recreate or not parquet_path.exists() + ] + + print(f"Reading files: {len(files_to_read)} new files of {len(h5_paths)} total.") + + # Initialize the indices for create_buffer_file conversion + self.reset_multi_index() + + if len(files_to_read) > 0: + error = Parallel(n_jobs=len(files_to_read), verbose=10)( + delayed(self.create_buffer_file)(h5_path, parquet_path) + for h5_path, parquet_path in files_to_read + ) + if any(error): + raise RuntimeError(f"Conversion failed for some files. {error}") + + # Raise an error if the conversion failed for any files + # TODO: merge this and the previous error trackings + if self.failed_files_error: + raise FileNotFoundError( + "Conversion failed for the following files:\n" + "\n".join(self.failed_files_error), + ) + + print("All files converted successfully!") + + def get_filled_dataframes(self): + # Read all parquet files into one dataframe using dask and reads the metadata and schema + dataframe = ddf.read_parquet(self.parquet_paths, calculate_divisions=True) + metadata = [pq.read_metadata(file) for file in self.parquet_paths] + # schema = [pq.read_schema(file) for file in self.parquet_paths] + + # Channels to fill NaN values + channels: list[str] = self.get_channels(["per_pulse", "per_train"]) + + overlap = min(file.num_rows for file in metadata) + + print("Filling nan values...") + dataframe = forward_fill_lazy( + df=dataframe, + columns=channels, + before=overlap, + iterations=self._config.get("forward_fill_iterations", 2), + ) + # Remove the NaNs from per_electron channels + dataframe_electron = dataframe.dropna( + subset=self.get_channels(["per_electron"]), + ) + dataframe_pulse = dataframe[ + self.multi_index + self.get_channels(["per_pulse", "per_train"]) + ] + dataframe_pulse = dataframe_pulse[ + (dataframe_pulse["electronId"] == 0) | (np.isnan(dataframe_pulse["electronId"])) + ] + return dataframe_electron, dataframe_pulse diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py new file mode 100644 index 00000000..679b8788 --- /dev/null +++ b/sed/loader/fel/dataframe.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +from functools import reduce +from pathlib import Path +from typing import TypeVar + +import dask.dataframe as ddf +import h5py +import numpy as np +from pandas import DataFrame +from pandas import MultiIndex +from pandas import Series + +from sed.loader.fel.multiindex import MultiIndexCreator +from sed.loader.utils import parse_h5_keys +from sed.loader.utils import split_dld_time_from_sector_id + +DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) + + +class DataFrameCreator(MultiIndexCreator): + def __init__(self, config_dataframe: dict) -> None: + """ + Initializes the DataFrameCreator class. + + Args: + config (dict): The configuration dictionary with only dataframe key. + """ + self.multi_index = ["trainId", "pulseId", "electronId"] + self.index_per_electron: MultiIndex = None + self.index_per_pulse: MultiIndex = None + self.failed_files_error: list[str] = [] + self._config = config_dataframe + + @property + def available_channels(self) -> list: + """Returns the channel names that are available for use, + excluding pulseId, defined by the json file""" + available_channels = list(self._config["channels"].keys()) + available_channels.remove("pulseId") + return available_channels + + def get_channels(self, formats: str | list[str] = "", index: bool = False) -> list[str]: + """ + Returns a list of channels associated with the specified format(s). + + Args: + formats (Union[str, List[str]]): The desired format(s) + ('per_pulse', 'per_electron', 'per_train', 'all'). + index (bool): If True, includes channels from the multi_index. + + Returns: + List[str]: A list of channels with the specified format(s). + """ + # If 'formats' is a single string, convert it to a list for uniform processing. + if isinstance(formats, str): + formats = [formats] + + # If 'formats' is a string "all", gather all possible formats. + if formats == ["all"]: + channels = self.get_channels(["per_pulse", "per_train", "per_electron"], index) + return channels + + channels = [] + for format_ in formats: + # Gather channels based on the specified format(s). + channels.extend( + key + for key in self.available_channels + if self._config["channels"][key]["format"] == format_ and key != "dldAux" + ) + # Include 'dldAuxChannels' if the format is 'per_pulse'. + if format_ == "per_pulse": + channels.extend( + self._config["channels"]["dldAux"]["dldAuxChannels"].keys(), + ) + + # Include channels from multi_index if 'index' is True. + if index: + channels.extend(self.multi_index) + + return channels + + def create_numpy_array_per_channel( + self, + h5_file: h5py.File, + channel: str, + ) -> tuple[Series, np.ndarray]: + """ + Returns a numpy array for a given channel name for a given file. + + Args: + h5_file (h5py.File): The h5py file object. + channel (str): The name of the channel. + + Returns: + Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array + for the channel's data. + + """ + # Get the data from the necessary h5 file and channel + group = h5_file[self._config["channels"][channel]["group_name"]] + + channel_dict = self._config["channels"][channel] # channel parameters + + train_id = Series(group["index"], name="trainId") # macrobunch + + # unpacks the timeStamp or value + if channel == "timeStamp": + np_array = group["time"][()] + else: + np_array = group["value"][()] + + # Use predefined axis and slice from the json file + # to choose correct dimension for necessary channel + if "slice" in channel_dict: + np_array = np.take( + np_array, + channel_dict["slice"], + axis=1, + ) + return train_id, np_array + + def create_dataframe_per_electron( + self, + np_array: np.ndarray, + train_id: Series, + channel: str, + ) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per electron]. + + Args: + np_array (np.ndarray): The numpy array containing the channel data. + train_id (Series): The train ID Series. + channel (str): The name of the channel. + + Returns: + DataFrame: The pandas DataFrame for the channel's data. + + Notes: + The microbunch resolved data is exploded and converted to a DataFrame. The MultiIndex + is set, and the NaN values are dropped, alongside the pulseId = 0 (meaningless). + + """ + return ( + Series((np_array[i] for i in train_id.index), name=channel) + .explode() + .dropna() + .to_frame() + .set_index(self.index_per_electron) + .drop( + index=np.arange(-self._config["ubid_offset"], 0), + level=1, + errors="ignore", + ) + ) + + def create_dataframe_per_pulse( + self, + np_array: np.ndarray, + train_id: Series, + channel: str, + channel_dict: dict, + ) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per pulse]. + + Args: + np_array (np.ndarray): The numpy array containing the channel data. + train_id (Series): The train ID Series. + channel (str): The name of the channel. + channel_dict (dict): The dictionary containing channel parameters. + + Returns: + DataFrame: The pandas DataFrame for the channel's data. + + Notes: + - For auxillary channels, the macrobunch resolved data is repeated 499 times to be + compared to electron resolved data for each auxillary channel. The data is then + converted to a multicolumn DataFrame. + - For all other pulse resolved channels, the macrobunch resolved data is exploded + to a DataFrame and the MultiIndex is set. + + """ + + # Special case for auxillary channels + if channel == "dldAux": + # Checks the channel dictionary for correct slices and creates a multicolumn DataFrame + data_frames = ( + Series( + (np_array[i, value] for i in train_id.index), + name=key, + index=train_id, + ).to_frame() + for key, value in channel_dict["dldAuxChannels"].items() + ) + + # Multiindex set and combined dataframe returned + data = reduce(DataFrame.combine_first, data_frames) + + # For all other pulse resolved channels + else: + # Macrobunch resolved data is exploded to a DataFrame and the MultiIndex is set + + # Creates the index_per_pulse for the given channel + self.create_multi_index_per_pulse(train_id, np_array) + data = ( + Series((np_array[i] for i in train_id.index), name=channel) + .explode() + .to_frame() + .set_index(self.index_per_pulse) + ) + + return data + + def create_dataframe_per_train( + self, + np_array: np.ndarray, + train_id: Series, + channel: str, + ) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per train]. + + Args: + np_array (np.ndarray): The numpy array containing the channel data. + train_id (Series): The train ID Series. + channel (str): The name of the channel. + + Returns: + DataFrame: The pandas DataFrame for the channel's data. + """ + return ( + Series((np_array[i] for i in train_id.index), name=channel) + .to_frame() + .set_index(train_id) + ) + + def create_dataframe_per_channel( + self, + h5_file: h5py.File, + channel: str, + ) -> Series | DataFrame: + """ + Returns a pandas DataFrame for a given channel name from a given file. + + This method takes an h5py.File object `h5_file` and a channel name `channel`, and returns + a pandas DataFrame containing the data for that channel from the file. The format of the + DataFrame depends on the channel's format specified in the configuration. + + Args: + h5_file (h5py.File): The h5py.File object representing the HDF5 file. + channel (str): The name of the channel. + + Returns: + Union[Series, DataFrame]: A pandas Series or DataFrame representing the channel's data. + + Raises: + ValueError: If the channel has an undefined format. + + """ + [train_id, np_array] = self.create_numpy_array_per_channel( + h5_file, + channel, + ) # numpy Array created + channel_dict = self._config["channels"][channel] # channel parameters + + # If np_array is size zero, fill with NaNs + if np_array.size == 0: + # Fill the np_array with NaN values of the same shape as train_id + np_array = np.full_like(train_id, np.nan, dtype=np.double) + # Create a Series using np_array, with train_id as the index + data = Series( + (np_array[i] for i in train_id.index), + name=channel, + index=train_id, + ) + + # Electron resolved data is treated here + if channel_dict["format"] == "per_electron": + # If index_per_electron is None, create it for the given file + if self.index_per_electron is None: + self.create_multi_index_per_electron( + self.create_numpy_array_per_channel( + h5_file, + "pulseId", + self._config["ubid_offset"], + ), + ) + + # Create a DataFrame for electron-resolved data + data = self.create_dataframe_per_electron( + np_array, + train_id, + channel, + ) + + # Pulse resolved data is treated here + elif channel_dict["format"] == "per_pulse": + # Create a DataFrame for pulse-resolved data + data = self.create_dataframe_per_pulse( + np_array, + train_id, + channel, + channel_dict, + ) + + # Train resolved data is treated here + elif channel_dict["format"] == "per_train": + # Create a DataFrame for train-resolved data + data = self.create_dataframe_per_train(np_array, train_id, channel) + + else: + raise ValueError( + channel + + "has an undefined format. Available formats are \ + per_pulse, per_electron and per_train", + ) + + return data + + def concatenate_channels( + self, + h5_file: h5py.File, + ) -> DataFrame: + """ + Concatenates the channels from the provided h5py.File into a pandas DataFrame. + + This method takes an h5py.File object `h5_file` and concatenates the channels present in + the file into a single pandas DataFrame. The concatenation is performed based on the + available channels specified in the configuration. + + Args: + h5_file (h5py.File): The h5py.File object representing the HDF5 file. + + Returns: + DataFrame: A concatenated pandas DataFrame containing the channels. + + Raises: + ValueError: If the group_name for any channel does not exist in the file. + + """ + all_keys = parse_h5_keys(h5_file) # Parses all channels present + + # Check for if the provided group_name actually exists in the file + for channel in self._config["channels"]: + if channel == "timeStamp": + group_name = self._config["channels"][channel]["group_name"] + "time" + else: + group_name = self._config["channels"][channel]["group_name"] + "value" + + if group_name not in all_keys: + raise ValueError( + f"The group_name for channel {channel} does not exist.", + ) + + # Create a generator expression to generate data frames for each channel + data_frames = ( + self.create_dataframe_per_channel(h5_file, each) for each in self.available_channels + ) + + # Use the reduce function to join the data frames into a single DataFrame + return reduce( + lambda left, right: left.join(right, how="outer"), + data_frames, + ) + + def create_dataframe_per_file( + self, + file_path: Path, + ) -> DataFrame: + """ + Create pandas DataFrames for the given file. + + This method loads an HDF5 file specified by `file_path` and constructs a pandas DataFrame + from the datasets within the file. The order of datasets in the DataFrames is the opposite + of the order specified by channel names. + + Args: + file_path (Path): Path to the input HDF5 file. + + Returns: + DataFrame: pandas DataFrame + + """ + # Loads h5 file and creates a dataframe + with h5py.File(file_path, "r") as h5_file: + self.reset_multi_index() # Reset MultiIndexes for next file + df = self.concatenate_channels(h5_file) + df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) + # correct the 3 bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + df = split_dld_time_from_sector_id(df, config=self._config) + return df diff --git a/sed/loader/fel/multiindex.py b/sed/loader/fel/multiindex.py new file mode 100644 index 00000000..691cb827 --- /dev/null +++ b/sed/loader/fel/multiindex.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import TypeVar + +import dask.dataframe as ddf +import numpy as np +from pandas import DataFrame +from pandas import MultiIndex +from pandas import Series + +DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) + + +class MultiIndexCreator: + def reset_multi_index(self) -> None: + """Resets the index per pulse and electron""" + self.index_per_electron = None + self.index_per_pulse = None + + def create_multi_index_per_electron(self, train_id, np_array, ubid_offset) -> None: + """ + Creates an index per electron using pulseId for usage with the electron + resolved pandas DataFrame. + + Args: + train_id (Series): The train ID Series. + np_array (np.ndarray): The numpy array containing the pulseId data. + + Notes: + - This method relies on the 'pulseId' channel to determine + the macrobunch IDs. + - It creates a MultiIndex with trainId, pulseId, and electronId + as the index levels. + """ + + # Create a series with the macrobunches as index and + # microbunches as values + macrobunches = ( + Series( + (np_array[i] for i in train_id.index), + name="pulseId", + index=train_id, + ) + - ubid_offset + ) + + # Explode dataframe to get all microbunch vales per macrobunch, + # remove NaN values and convert to type int + microbunches = macrobunches.explode().dropna().astype(int) + + # Create temporary index values + index_temp = MultiIndex.from_arrays( + (microbunches.index, microbunches.values), + names=["trainId", "pulseId"], + ) + + # Calculate the electron counts per pulseId unique preserves the order of appearance + electron_counts = index_temp.value_counts()[index_temp.unique()].values + + # Series object for indexing with electrons + electrons = ( + Series( + [np.arange(electron_counts[i]) for i in range(electron_counts.size)], + ) + .explode() + .astype(int) + ) + + # Create a pandas MultiIndex using the exploded datasets + self.index_per_electron = MultiIndex.from_arrays( + (microbunches.index, microbunches.values, electrons), + names=self.multi_index, + ) + + def create_multi_index_per_pulse( + self, + train_id: Series, + np_array: np.ndarray, + ) -> None: + """ + Creates an index per pulse using a pulse resolved channel's macrobunch ID, for usage with + the pulse resolved pandas DataFrame. + + Args: + train_id (Series): The train ID Series. + np_array (np.ndarray): The numpy array containing the pulse resolved data. + + Notes: + - This method creates a MultiIndex with trainId and pulseId as the index levels. + """ + + # Create a pandas MultiIndex, useful for comparing electron and + # pulse resolved dataframes + self.index_per_pulse = MultiIndex.from_product( + (train_id, np.arange(0, np_array.shape[1])), + names=["trainId", "pulseId"], + ) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py new file mode 100644 index 00000000..0e5ac9ec --- /dev/null +++ b/sed/loader/fel/parquet.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TypeVar + +import dask.dataframe as ddf +from pandas import DataFrame + + +DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) + + +class ParquetHandler: + """ + A handler for saving and reading Dask DataFrames to/from Parquet files. + + Args: + parquet_names Union[str, List[str]]: The base name of the Parquet files. + folder (Path): The directory where the Parquet file will be stored. + subfolder (str): Optional subfolder within the main folder. + prefix (str): Optional prefix for the Parquet file name. + suffix (str): Optional suffix for the Parquet file name. + parquet_path (Path): Optional custom path for the Parquet file. + """ + + def __init__( + self, + parquet_names: str | list[str], + folder: Path, + subfolder: str = "", + prefix: str = "", + suffix: str = "", + parquet_paths: Path | list[Path] = None, + ): + if parquet_paths is not None: + self.parquet_paths = parquet_paths + else: + # Create the full path for the Parquet file + parquet_dir = folder.joinpath(subfolder) + parquet_dir.mkdir(parents=True, exist_ok=True) + filenames = [ + Path(prefix + parquet_name + suffix).with_suffix(".parquet") + for parquet_name in parquet_names + ] + self.parquet_paths = [parquet_dir.joinpath(filename) for filename in filenames] + + def save_parquet(self, dfs: DFType | list[DFType]) -> None: + """ + Save the DataFrame to a Parquet file. + + Args: + df (DFType | List[DFType]): The Dask DataFrame to be saved. + """ + # Compute the Dask DataFrame, reset the index, and save to Parquet + for df, parquet_paths in zip(dfs, self.parquet_paths): + df.compute().reset_index(drop=True).to_parquet(parquet_paths) + print(f"Parquet file saved: {parquet_paths}") + + def read_parquet(self) -> ddf.DataFrame: + """ + Read a Dask DataFrame from the Parquet file. + + Returns: + ddf.DataFrame: The Dask DataFrame read from the Parquet file. + + Raises: + FileNotFoundError: If the Parquet file does not exist. + """ + try: + return ddf.read_parquet(self.parquet_paths, calculate_divisions=True) + except Exception as exc: + raise FileNotFoundError( + "The Parquet file does not exist. " + "If it is in another location, provide the correct path as parquet_path.", + ) from exc + + def delete_parquet(self) -> None: + """ + Delete the Parquet file. + """ + for parquet_path in self.parquet_paths: + parquet_path.unlink() + print(f"Parquet file deleted: {parquet_path}") diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 6e1afd97..7c8756ec 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -8,7 +8,6 @@ sed funtionality. """ import time -from functools import reduce from pathlib import Path from typing import List from typing import Sequence @@ -16,21 +15,12 @@ from typing import Union import dask.dataframe as dd -import h5py -import numpy as np -import pyarrow.parquet as pq -from joblib import delayed -from joblib import Parallel from natsort import natsorted -from pandas import DataFrame -from pandas import MultiIndex -from pandas import Series -from sed.core import dfops from sed.loader.base.loader import BaseLoader +from sed.loader.fel import BufferFileHandler +from sed.loader.fel import ParquetHandler from sed.loader.flash.metadata import MetadataRetriever -from sed.loader.utils import parse_h5_keys -from sed.loader.utils import split_dld_time_from_sector_id class FlashLoader(BaseLoader): @@ -46,10 +36,6 @@ class FlashLoader(BaseLoader): def __init__(self, config: dict) -> None: super().__init__(config=config) - self.multi_index = ["trainId", "pulseId", "electronId"] - self.index_per_electron: MultiIndex = None - self.index_per_pulse: MultiIndex = None - self.failed_files_error: List[str] = [] def initialize_paths(self) -> Tuple[List[Path], Path]: """ @@ -168,675 +154,7 @@ def get_files_from_run_id( # Return the list of found files return [str(file.resolve()) for file in files] - @property - def available_channels(self) -> List: - """Returns the channel names that are available for use, - excluding pulseId, defined by the json file""" - available_channels = list(self._config["dataframe"]["channels"].keys()) - available_channels.remove("pulseId") - return available_channels - - def get_channels(self, formats: Union[str, List[str]] = "", index: bool = False) -> List[str]: - """ - Returns a list of channels associated with the specified format(s). - - Args: - formats (Union[str, List[str]]): The desired format(s) - ('per_pulse', 'per_electron', 'per_train', 'all'). - index (bool): If True, includes channels from the multi_index. - - Returns: - List[str]: A list of channels with the specified format(s). - """ - # If 'formats' is a single string, convert it to a list for uniform processing. - if isinstance(formats, str): - formats = [formats] - - # If 'formats' is a string "all", gather all possible formats. - if formats == ["all"]: - channels = self.get_channels(["per_pulse", "per_train", "per_electron"], index) - return channels - - channels = [] - for format_ in formats: - # Gather channels based on the specified format(s). - channels.extend( - key - for key in self.available_channels - if self._config["dataframe"]["channels"][key]["format"] == format_ - and key != "dldAux" - ) - # Include 'dldAuxChannels' if the format is 'per_pulse'. - if format_ == "per_pulse": - channels.extend( - self._config["dataframe"]["channels"]["dldAux"]["dldAuxChannels"].keys(), - ) - - # Include channels from multi_index if 'index' is True. - if index: - channels.extend(self.multi_index) - - return channels - - def reset_multi_index(self) -> None: - """Resets the index per pulse and electron""" - self.index_per_electron = None - self.index_per_pulse = None - - def create_multi_index_per_electron(self, h5_file: h5py.File) -> None: - """ - Creates an index per electron using pulseId for usage with the electron - resolved pandas DataFrame. - - Args: - h5_file (h5py.File): The HDF5 file object. - - Notes: - - This method relies on the 'pulseId' channel to determine - the macrobunch IDs. - - It creates a MultiIndex with trainId, pulseId, and electronId - as the index levels. - """ - - # Macrobunch IDs obtained from the pulseId channel - [train_id, np_array] = self.create_numpy_array_per_channel( - h5_file, - "pulseId", - ) - - # Create a series with the macrobunches as index and - # microbunches as values - macrobunches = ( - Series( - (np_array[i] for i in train_id.index), - name="pulseId", - index=train_id, - ) - - self._config["dataframe"]["ubid_offset"] - ) - - # Explode dataframe to get all microbunch vales per macrobunch, - # remove NaN values and convert to type int - microbunches = macrobunches.explode().dropna().astype(int) - - # Create temporary index values - index_temp = MultiIndex.from_arrays( - (microbunches.index, microbunches.values), - names=["trainId", "pulseId"], - ) - - # Calculate the electron counts per pulseId unique preserves the order of appearance - electron_counts = index_temp.value_counts()[index_temp.unique()].values - - # Series object for indexing with electrons - electrons = ( - Series( - [np.arange(electron_counts[i]) for i in range(electron_counts.size)], - ) - .explode() - .astype(int) - ) - - # Create a pandas MultiIndex using the exploded datasets - self.index_per_electron = MultiIndex.from_arrays( - (microbunches.index, microbunches.values, electrons), - names=self.multi_index, - ) - - def create_multi_index_per_pulse( - self, - train_id: Series, - np_array: np.ndarray, - ) -> None: - """ - Creates an index per pulse using a pulse resolved channel's macrobunch ID, for usage with - the pulse resolved pandas DataFrame. - - Args: - train_id (Series): The train ID Series. - np_array (np.ndarray): The numpy array containing the pulse resolved data. - - Notes: - - This method creates a MultiIndex with trainId and pulseId as the index levels. - """ - - # Create a pandas MultiIndex, useful for comparing electron and - # pulse resolved dataframes - self.index_per_pulse = MultiIndex.from_product( - (train_id, np.arange(0, np_array.shape[1])), - names=["trainId", "pulseId"], - ) - - def create_numpy_array_per_channel( - self, - h5_file: h5py.File, - channel: str, - ) -> Tuple[Series, np.ndarray]: - """ - Returns a numpy array for a given channel name for a given file. - - Args: - h5_file (h5py.File): The h5py file object. - channel (str): The name of the channel. - - Returns: - Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array - for the channel's data. - - """ - # Get the data from the necessary h5 file and channel - group = h5_file[self._config["dataframe"]["channels"][channel]["group_name"]] - - channel_dict = self._config["dataframe"]["channels"][channel] # channel parameters - - train_id = Series(group["index"], name="trainId") # macrobunch - - # unpacks the timeStamp or value - if channel == "timeStamp": - np_array = group["time"][()] - else: - np_array = group["value"][()] - - # Use predefined axis and slice from the json file - # to choose correct dimension for necessary channel - if "slice" in channel_dict: - np_array = np.take( - np_array, - channel_dict["slice"], - axis=1, - ) - return train_id, np_array - - def create_dataframe_per_electron( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - ) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per electron]. - - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - - Returns: - DataFrame: The pandas DataFrame for the channel's data. - - Notes: - The microbunch resolved data is exploded and converted to a DataFrame. The MultiIndex - is set, and the NaN values are dropped, alongside the pulseId = 0 (meaningless). - - """ - return ( - Series((np_array[i] for i in train_id.index), name=channel) - .explode() - .dropna() - .to_frame() - .set_index(self.index_per_electron) - .drop( - index=np.arange(-self._config["dataframe"]["ubid_offset"], 0), - level=1, - errors="ignore", - ) - ) - - def create_dataframe_per_pulse( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - channel_dict: dict, - ) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per pulse]. - - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - channel_dict (dict): The dictionary containing channel parameters. - - Returns: - DataFrame: The pandas DataFrame for the channel's data. - - Notes: - - For auxillary channels, the macrobunch resolved data is repeated 499 times to be - compared to electron resolved data for each auxillary channel. The data is then - converted to a multicolumn DataFrame. - - For all other pulse resolved channels, the macrobunch resolved data is exploded - to a DataFrame and the MultiIndex is set. - - """ - - # Special case for auxillary channels - if channel == "dldAux": - # Checks the channel dictionary for correct slices and creates a multicolumn DataFrame - data_frames = ( - Series( - (np_array[i, value] for i in train_id.index), - name=key, - index=train_id, - ).to_frame() - for key, value in channel_dict["dldAuxChannels"].items() - ) - - # Multiindex set and combined dataframe returned - data = reduce(DataFrame.combine_first, data_frames) - - # For all other pulse resolved channels - else: - # Macrobunch resolved data is exploded to a DataFrame and the MultiIndex is set - - # Creates the index_per_pulse for the given channel - self.create_multi_index_per_pulse(train_id, np_array) - data = ( - Series((np_array[i] for i in train_id.index), name=channel) - .explode() - .to_frame() - .set_index(self.index_per_pulse) - ) - - return data - - def create_dataframe_per_train( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - ) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per train]. - - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - - Returns: - DataFrame: The pandas DataFrame for the channel's data. - """ - return ( - Series((np_array[i] for i in train_id.index), name=channel) - .to_frame() - .set_index(train_id) - ) - - def create_dataframe_per_channel( - self, - h5_file: h5py.File, - channel: str, - ) -> Union[Series, DataFrame]: - """ - Returns a pandas DataFrame for a given channel name from a given file. - - This method takes an h5py.File object `h5_file` and a channel name `channel`, and returns - a pandas DataFrame containing the data for that channel from the file. The format of the - DataFrame depends on the channel's format specified in the configuration. - - Args: - h5_file (h5py.File): The h5py.File object representing the HDF5 file. - channel (str): The name of the channel. - - Returns: - Union[Series, DataFrame]: A pandas Series or DataFrame representing the channel's data. - - Raises: - ValueError: If the channel has an undefined format. - - """ - [train_id, np_array] = self.create_numpy_array_per_channel( - h5_file, - channel, - ) # numpy Array created - channel_dict = self._config["dataframe"]["channels"][channel] # channel parameters - - # If np_array is size zero, fill with NaNs - if np_array.size == 0: - # Fill the np_array with NaN values of the same shape as train_id - np_array = np.full_like(train_id, np.nan, dtype=np.double) - # Create a Series using np_array, with train_id as the index - data = Series( - (np_array[i] for i in train_id.index), - name=channel, - index=train_id, - ) - - # Electron resolved data is treated here - if channel_dict["format"] == "per_electron": - # If index_per_electron is None, create it for the given file - if self.index_per_electron is None: - self.create_multi_index_per_electron(h5_file) - - # Create a DataFrame for electron-resolved data - data = self.create_dataframe_per_electron( - np_array, - train_id, - channel, - ) - - # Pulse resolved data is treated here - elif channel_dict["format"] == "per_pulse": - # Create a DataFrame for pulse-resolved data - data = self.create_dataframe_per_pulse( - np_array, - train_id, - channel, - channel_dict, - ) - - # Train resolved data is treated here - elif channel_dict["format"] == "per_train": - # Create a DataFrame for train-resolved data - data = self.create_dataframe_per_train(np_array, train_id, channel) - - else: - raise ValueError( - channel - + "has an undefined format. Available formats are \ - per_pulse, per_electron and per_train", - ) - - return data - - def concatenate_channels( - self, - h5_file: h5py.File, - ) -> DataFrame: - """ - Concatenates the channels from the provided h5py.File into a pandas DataFrame. - - This method takes an h5py.File object `h5_file` and concatenates the channels present in - the file into a single pandas DataFrame. The concatenation is performed based on the - available channels specified in the configuration. - - Args: - h5_file (h5py.File): The h5py.File object representing the HDF5 file. - - Returns: - DataFrame: A concatenated pandas DataFrame containing the channels. - - Raises: - ValueError: If the group_name for any channel does not exist in the file. - - """ - all_keys = parse_h5_keys(h5_file) # Parses all channels present - - # Check for if the provided group_name actually exists in the file - for channel in self._config["dataframe"]["channels"]: - if channel == "timeStamp": - group_name = self._config["dataframe"]["channels"][channel]["group_name"] + "time" - else: - group_name = self._config["dataframe"]["channels"][channel]["group_name"] + "value" - - if group_name not in all_keys: - raise ValueError( - f"The group_name for channel {channel} does not exist.", - ) - - # Create a generator expression to generate data frames for each channel - data_frames = ( - self.create_dataframe_per_channel(h5_file, each) for each in self.available_channels - ) - - # Use the reduce function to join the data frames into a single DataFrame - return reduce( - lambda left, right: left.join(right, how="outer"), - data_frames, - ) - - def create_dataframe_per_file( - self, - file_path: Path, - ) -> DataFrame: - """ - Create pandas DataFrames for the given file. - - This method loads an HDF5 file specified by `file_path` and constructs a pandas DataFrame - from the datasets within the file. The order of datasets in the DataFrames is the opposite - of the order specified by channel names. - - Args: - file_path (Path): Path to the input HDF5 file. - - Returns: - DataFrame: pandas DataFrame - - """ - # Loads h5 file and creates a dataframe - with h5py.File(file_path, "r") as h5_file: - self.reset_multi_index() # Reset MultiIndexes for next file - df = self.concatenate_channels(h5_file) - df = df.dropna(subset=self._config["dataframe"].get("tof_column", "dldTimeSteps")) - # correct the 3 bit shift which encodes the detector ID in the 8s time - if self._config["dataframe"].get("split_sector_id_from_dld_time", False): - df = split_dld_time_from_sector_id(df, config=self._config) - return df - - def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, Exception]: - """ - Converts an HDF5 file to Parquet format to create a buffer file. - - This method uses `create_dataframe_per_file` method to create dataframes from individual - files within an HDF5 file. The resulting dataframe is then saved to a Parquet file. - - Args: - h5_path (Path): Path to the input HDF5 file. - parquet_path (Path): Path to the output Parquet file. - - Raises: - ValueError: If an error occurs during the conversion process. - - """ - try: - ( - self.create_dataframe_per_file(h5_path) - .reset_index(level=self.multi_index) - .to_parquet(parquet_path, index=False) - ) - except Exception as exc: # pylint: disable=broad-except - self.failed_files_error.append(f"{parquet_path}: {type(exc)} {exc}") - return exc - return None - - def buffer_file_handler( - self, - data_parquet_dir: Path, - detector: str, - force_recreate: bool, - ) -> Tuple[List[Path], List, List]: - """ - Handles the conversion of buffer files (h5 to parquet) and returns the filenames. - - Args: - data_parquet_dir (Path): Directory where the parquet files will be stored. - detector (str): Detector name. - force_recreate (bool): Forces recreation of buffer files - - Returns: - Tuple[List[Path], List, List]: Three lists, one for - parquet file paths, one for metadata and one for schema. - - Raises: - FileNotFoundError: If the conversion fails for any files or no data is available. - """ - - # Create the directory for buffer parquet files - buffer_file_dir = data_parquet_dir.joinpath("buffer") - buffer_file_dir.mkdir(parents=True, exist_ok=True) - - # Create two separate lists for h5 and parquet file paths - h5_filenames = [Path(file) for file in self.files] - parquet_filenames = [ - buffer_file_dir.joinpath(Path(file).stem + detector) for file in self.files - ] - existing_parquet_filenames = [file for file in parquet_filenames if file.exists()] - - # Raise a value error if no data is available after the conversion - if len(h5_filenames) == 0: - raise ValueError("No data available. Probably failed reading all h5 files") - - if not force_recreate: - # Check if the available channels match the schema of the existing parquet files - parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set(self.get_channels(formats="all", index=True)) - if self._config["dataframe"].get("split_sector_id_from_dld_time", False): - config_schema.add(self._config["dataframe"].get("sector_id_column", False)) - - for i, schema in enumerate(parquet_schemas): - schema_set = set(schema.names) - if schema_set != config_schema: - missing_in_parquet = config_schema - schema_set - missing_in_config = schema_set - config_schema - - missing_in_parquet_str = ( - f"Missing in parquet: {missing_in_parquet}" if missing_in_parquet else "" - ) - missing_in_config_str = ( - f"Missing in config: {missing_in_config}" if missing_in_config else "" - ) - - raise ValueError( - "The available channels do not match the schema of file", - f"{existing_parquet_filenames[i]}", - f"{missing_in_parquet_str}", - f"{missing_in_config_str}", - "Please check the configuration file or set force_recreate to True.", - ) - - # Choose files to read - files_to_read = [ - (h5_path, parquet_path) - for h5_path, parquet_path in zip(h5_filenames, parquet_filenames) - if force_recreate or not parquet_path.exists() - ] - - print(f"Reading files: {len(files_to_read)} new files of {len(h5_filenames)} total.") - - # Initialize the indices for create_buffer_file conversion - self.reset_multi_index() - - # Convert the remaining h5 files to parquet in parallel if there are any - if len(files_to_read) > 0: - error = Parallel(n_jobs=len(files_to_read), verbose=10)( - delayed(self.create_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in files_to_read - ) - if any(error): - raise RuntimeError(f"Conversion failed for some files. {error}") - - # Raise an error if the conversion failed for any files - # TODO: merge this and the previous error trackings - if self.failed_files_error: - raise FileNotFoundError( - "Conversion failed for the following files:\n" + "\n".join(self.failed_files_error), - ) - - print("All files converted successfully!") - - # read all parquet metadata and schema - metadata = [pq.read_metadata(file) for file in parquet_filenames] - schema = [pq.read_schema(file) for file in parquet_filenames] - - return parquet_filenames, metadata, schema - - def parquet_handler( - self, - data_parquet_dir: Path, - detector: str = "", - parquet_path: Path = None, - converted: bool = False, - load_parquet: bool = False, - save_parquet: bool = False, - force_recreate: bool = False, - **kwds, - ) -> Tuple[dd.DataFrame, dd.DataFrame]: - """ - Handles loading and saving of parquet files based on the provided parameters. - - Args: - data_parquet_dir (Path): Directory where the parquet files are located. - detector (str, optional): Adds a identifier for parquets to distinguish multidetector - systems. - parquet_path (str, optional): Path to the combined parquet file. - converted (bool, optional): True if data is augmented by adding additional columns - externally and saved into converted folder. - load_parquet (bool, optional): Loads the entire parquet into the dd dataframe. - save_parquet (bool, optional): Saves the entire dataframe into a parquet. - force_recreate (bool, optional): Forces recreation of buffer file. - Returns: - tuple: A tuple containing two dataframes: - - dataframe_electron: Dataframe containing the loaded/augmented electron data. - - dataframe_pulse: Dataframe containing the loaded/augmented timed data. - - Raises: - FileNotFoundError: If the requested parquet file is not found. - - """ - - # Construct the parquet path if not provided - if parquet_path is None: - parquet_name = "_".join(str(run) for run in self.runs) - parquet_dir = data_parquet_dir.joinpath("converted") if converted else data_parquet_dir - - parquet_path = parquet_dir.joinpath( - "run_" + parquet_name + detector, - ).with_suffix(".parquet") - - # Check if load_parquet is flagged and then load the file if it exists - if load_parquet: - try: - dataframe = dd.read_parquet(parquet_path) - except Exception as exc: - raise FileNotFoundError( - "The final parquet for this run(s) does not exist yet. " - "If it is in another location, please provide the path as parquet_path.", - ) from exc - - else: - # Obtain the parquet filenames, metadata and schema from the method - # which handles buffer file creation/reading - filenames, metadata, _ = self.buffer_file_handler( - data_parquet_dir, - detector, - force_recreate, - ) - - # Read all parquet files into one dataframe using dask - dataframe = dd.read_parquet(filenames, calculate_divisions=True) - - # Channels to fill NaN values - channels: List[str] = self.get_channels(["per_pulse", "per_train"]) - - overlap = min(file.num_rows for file in metadata) - - print("Filling nan values...") - dataframe = dfops.forward_fill_lazy( - df=dataframe, - columns=channels, - before=overlap, - iterations=self._config["dataframe"].get("forward_fill_iterations", 2), - ) - # Remove the NaNs from per_electron channels - dataframe_electron = dataframe.dropna( - subset=self.get_channels(["per_electron"]), - ) - dataframe_pulse = dataframe[ - self.multi_index + self.get_channels(["per_pulse", "per_train"]) - ] - dataframe_pulse = dataframe_pulse[ - (dataframe_pulse["electronId"] == 0) | (np.isnan(dataframe_pulse["electronId"])) - ] - - # Save the dataframe as parquet if requested - if save_parquet: - dataframe_electron.compute().reset_index(drop=True).to_parquet(parquet_path) - print("Combined parquet file saved.") - - return dataframe_electron, dataframe_pulse - - def parse_metadata(self, scicat_token: str = None, **kwds) -> dict: + def parse_metadata(self) -> dict: """Uses the MetadataRetriever class to fetch metadata from scicat for each run. Returns: @@ -870,6 +188,11 @@ def read_dataframe( ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, + converted: bool = False, + load_parquet: bool = False, + save_parquet: bool = False, + detector: str = "", + force_recreate: bool = False, **kwds, ) -> Tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -924,7 +247,29 @@ def read_dataframe( metadata=metadata, ) - df, df_timed = self.parquet_handler(data_parquet_dir, **kwds) + filename = "_".join(str(run) for run in self.runs) + converted_str = "converted" if converted else "" + prq = ParquetHandler(filename, data_parquet_dir, converted_str, "run_", detector) + + # Check if load_parquet is flagged and then load the file if it exists + if load_parquet: + prq.read_parquet() + + else: + # Obtain the parquet filenames, metadata and schema from the method + # which handles buffer file creation/reading + h5_paths = [Path(file) for file in self.files] + bfh = BufferFileHandler( + self._config["dataframe"], + h5_paths, + data_parquet_dir, + force_recreate, + suffix=detector, + ) + df, df_timed = bfh.get_filled_dataframes() + # Save the dataframe as parquet if requested + if save_parquet: + prq.save_parquet(df) metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") From f376387e313fb8bfd57ef278f3fc93491c124881 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 25 Nov 2023 14:52:05 +0100 Subject: [PATCH 002/300] update dataframe class to be able to use index and dataset keys --- sed/loader/fel/dataframe.py | 91 +++++++++++++++++++----------------- sed/loader/fel/multiindex.py | 42 +++++++++-------- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 679b8788..63126d79 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -2,40 +2,36 @@ from functools import reduce from pathlib import Path -from typing import TypeVar -import dask.dataframe as ddf import h5py import numpy as np from pandas import DataFrame -from pandas import MultiIndex from pandas import Series from sed.loader.fel.multiindex import MultiIndexCreator from sed.loader.utils import parse_h5_keys from sed.loader.utils import split_dld_time_from_sector_id -DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) - class DataFrameCreator(MultiIndexCreator): + """ + Utility class for creating pandas DataFrames from HDF5 files with multiple channels. + """ + def __init__(self, config_dataframe: dict) -> None: """ Initializes the DataFrameCreator class. Args: - config (dict): The configuration dictionary with only dataframe key. + config_dataframe (dict): The configuration dictionary with only the dataframe key. """ - self.multi_index = ["trainId", "pulseId", "electronId"] - self.index_per_electron: MultiIndex = None - self.index_per_pulse: MultiIndex = None + super().__init__() self.failed_files_error: list[str] = [] self._config = config_dataframe @property def available_channels(self) -> list: - """Returns the channel names that are available for use, - excluding pulseId, defined by the json file""" + """Returns the channel names that are available for use, excluding pulseId.""" available_channels = list(self._config["channels"].keys()) available_channels.remove("pulseId") return available_channels @@ -46,7 +42,7 @@ def get_channels(self, formats: str | list[str] = "", index: bool = False) -> li Args: formats (Union[str, List[str]]): The desired format(s) - ('per_pulse', 'per_electron', 'per_train', 'all'). + ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multi_index. Returns: @@ -81,6 +77,38 @@ def get_channels(self, formats: str | list[str] = "", index: bool = False) -> li return channels + def get_index_dataset_key(self, channel: str) -> tuple[str, str]: + """ + Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. + + Args: + channel (str): The name of the channel. + + Returns: + tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. + + Raises: + ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + """ + channel_config = self._config["channels"][channel] + + if "group_name" in channel_config: + index_key = channel_config["group_name"]["index"] + if channel == "timeStamp": + dataset_key = channel_config["group_name"]["time"] + else: + dataset_key = channel_config["group_name"]["value"] + return index_key, dataset_key + elif "index_key" in channel_config and "dataset_key" in channel_config: + return channel_config["index_key"], channel_config["dataset_key"] + else: + raise ValueError( + "For channel:", + channel, + "Provide either both 'index_key' and 'dataset_key'.", + "or 'group_name' (allows only 'index' and 'value' or 'time' keys.)", + ) + def create_numpy_array_per_channel( self, h5_file: h5py.File, @@ -96,20 +124,14 @@ def create_numpy_array_per_channel( Returns: Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array for the channel's data. - """ # Get the data from the necessary h5 file and channel - group = h5_file[self._config["channels"][channel]["group_name"]] + index_key, dataset_key = self.get_index_dataset_key(channel) channel_dict = self._config["channels"][channel] # channel parameters - train_id = Series(group["index"], name="trainId") # macrobunch - - # unpacks the timeStamp or value - if channel == "timeStamp": - np_array = group["time"][()] - else: - np_array = group["value"][()] + train_id = Series(h5_file[index_key], name="trainId") # macrobunch + np_array = h5_file[dataset_key][()] # Use predefined axis and slice from the json file # to choose correct dimension for necessary channel @@ -137,11 +159,6 @@ def create_dataframe_per_electron( Returns: DataFrame: The pandas DataFrame for the channel's data. - - Notes: - The microbunch resolved data is exploded and converted to a DataFrame. The MultiIndex - is set, and the NaN values are dropped, alongside the pulseId = 0 (meaningless). - """ return ( Series((np_array[i] for i in train_id.index), name=channel) @@ -174,16 +191,7 @@ def create_dataframe_per_pulse( Returns: DataFrame: The pandas DataFrame for the channel's data. - - Notes: - - For auxillary channels, the macrobunch resolved data is repeated 499 times to be - compared to electron resolved data for each auxillary channel. The data is then - converted to a multicolumn DataFrame. - - For all other pulse resolved channels, the macrobunch resolved data is exploded - to a DataFrame and the MultiIndex is set. - """ - # Special case for auxillary channels if channel == "dldAux": # Checks the channel dictionary for correct slices and creates a multicolumn DataFrame @@ -202,7 +210,6 @@ def create_dataframe_per_pulse( # For all other pulse resolved channels else: # Macrobunch resolved data is exploded to a DataFrame and the MultiIndex is set - # Creates the index_per_pulse for the given channel self.create_multi_index_per_pulse(train_id, np_array) data = ( @@ -258,7 +265,6 @@ def create_dataframe_per_channel( Raises: ValueError: If the channel has an undefined format. - """ [train_id, np_array] = self.create_numpy_array_per_channel( h5_file, @@ -313,9 +319,8 @@ def create_dataframe_per_channel( else: raise ValueError( - channel - + "has an undefined format. Available formats are \ - per_pulse, per_electron and per_train", + f"{channel} has an undefined format", + "Available formats are per_pulse, per_electron and per_train", ) return data @@ -339,7 +344,6 @@ def concatenate_channels( Raises: ValueError: If the group_name for any channel does not exist in the file. - """ all_keys = parse_h5_keys(h5_file) # Parses all channels present @@ -382,14 +386,13 @@ def create_dataframe_per_file( Returns: DataFrame: pandas DataFrame - """ # Loads h5 file and creates a dataframe with h5py.File(file_path, "r") as h5_file: - self.reset_multi_index() # Reset MultiIndexes for next file + self.reset_multi_index() # Reset MultiIndexes for the next file df = self.concatenate_channels(h5_file) df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) - # correct the 3 bit shift which encodes the detector ID in the 8s time + # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): df = split_dld_time_from_sector_id(df, config=self._config) return df diff --git a/sed/loader/fel/multiindex.py b/sed/loader/fel/multiindex.py index 691cb827..f854b0f5 100644 --- a/sed/loader/fel/multiindex.py +++ b/sed/loader/fel/multiindex.py @@ -1,40 +1,45 @@ -from __future__ import annotations - -from typing import TypeVar - -import dask.dataframe as ddf import numpy as np -from pandas import DataFrame from pandas import MultiIndex from pandas import Series -DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) - class MultiIndexCreator: + """ + Utility class for creating MultiIndex for electron and pulse resolved DataFrames. + """ + + def __init__(self) -> None: + self.index_per_electron: MultiIndex = None + self.index_per_pulse: MultiIndex = None + self.multi_index = ["trainId", "pulseId", "electronId"] + def reset_multi_index(self) -> None: - """Resets the index per pulse and electron""" + """Resets the index per pulse and electron.""" self.index_per_electron = None self.index_per_pulse = None - def create_multi_index_per_electron(self, train_id, np_array, ubid_offset) -> None: + def create_multi_index_per_electron( + self, + train_id: Series, + np_array: np.ndarray, + ubid_offset: int, + ) -> None: """ Creates an index per electron using pulseId for usage with the electron - resolved pandas DataFrame. + resolved pandas DataFrame. Args: train_id (Series): The train ID Series. np_array (np.ndarray): The numpy array containing the pulseId data. + ubid_offset (int): The offset for adjusting pulseId. Notes: - This method relies on the 'pulseId' channel to determine - the macrobunch IDs. + the macrobunch IDs. - It creates a MultiIndex with trainId, pulseId, and electronId - as the index levels. + as the index levels. """ - - # Create a series with the macrobunches as index and - # microbunches as values + # Calculate macrobunches macrobunches = ( Series( (np_array[i] for i in train_id.index), @@ -44,7 +49,7 @@ def create_multi_index_per_electron(self, train_id, np_array, ubid_offset) -> No - ubid_offset ) - # Explode dataframe to get all microbunch vales per macrobunch, + # Explode dataframe to get all microbunch values per macrobunch, # remove NaN values and convert to type int microbunches = macrobunches.explode().dropna().astype(int) @@ -54,7 +59,7 @@ def create_multi_index_per_electron(self, train_id, np_array, ubid_offset) -> No names=["trainId", "pulseId"], ) - # Calculate the electron counts per pulseId unique preserves the order of appearance + # Calculate electron counts per pulseId; unique preserves the order of appearance electron_counts = index_temp.value_counts()[index_temp.unique()].values # Series object for indexing with electrons @@ -88,7 +93,6 @@ def create_multi_index_per_pulse( Notes: - This method creates a MultiIndex with trainId and pulseId as the index levels. """ - # Create a pandas MultiIndex, useful for comparing electron and # pulse resolved dataframes self.index_per_pulse = MultiIndex.from_product( From 08e8d9f5b5298541c5f629f7f8a90505642f64d7 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 15:52:34 +0100 Subject: [PATCH 003/300] minor changes introduced --- sed/loader/flash/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 7c8756ec..ff757209 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -266,10 +266,10 @@ def read_dataframe( force_recreate, suffix=detector, ) - df, df_timed = bfh.get_filled_dataframes() + df, df_timed = bfh.get_filled_dataframe() # Save the dataframe as parquet if requested if save_parquet: - prq.save_parquet(df) + prq.save_parquet(df, drop_index=True) metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") From 5c9a04c9af20ade5bfd490731ac45038a90dde5e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 15:56:52 +0100 Subject: [PATCH 004/300] change majorly the class with a new initialize method. now save parquet accepts parquet_paths which is useful for loading only subset of files --- sed/loader/fel/parquet.py | 69 +++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 0e5ac9ec..3ce8d35a 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -1,15 +1,11 @@ from __future__ import annotations from pathlib import Path -from typing import TypeVar import dask.dataframe as ddf from pandas import DataFrame -DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) - - class ParquetHandler: """ A handler for saving and reading Dask DataFrames to/from Parquet files. @@ -25,36 +21,61 @@ class ParquetHandler: def __init__( self, - parquet_names: str | list[str], + folder=None, + parquet_names=None, + subfolder=None, + prefix=None, + suffix=None, + parquet_paths=None, + ): + + self.parquet_paths: Path | list[Path] = None + + if not folder and not parquet_paths: + raise ValueError("Please provide folder or parquet_paths.") + if folder and not parquet_names: + raise ValueError("With folder, please provide parquet_names.") + + if parquet_paths: + self.parquet_paths: Path | list[Path] = parquet_paths + else: + self._initialize_paths(folder, parquet_names, subfolder, prefix, suffix) + + def _initialize_paths( + self, folder: Path, + parquet_names: str | list[str], subfolder: str = "", prefix: str = "", suffix: str = "", - parquet_paths: Path | list[Path] = None, - ): - if parquet_paths is not None: - self.parquet_paths = parquet_paths - else: - # Create the full path for the Parquet file - parquet_dir = folder.joinpath(subfolder) - parquet_dir.mkdir(parents=True, exist_ok=True) - filenames = [ - Path(prefix + parquet_name + suffix).with_suffix(".parquet") - for parquet_name in parquet_names - ] - self.parquet_paths = [parquet_dir.joinpath(filename) for filename in filenames] - - def save_parquet(self, dfs: DFType | list[DFType]) -> None: + ) -> None: + """ + Create the directory for the Parquet file. + """ + # Create the full path for the Parquet file + parquet_dir = folder.joinpath(subfolder) + parquet_dir.mkdir(parents=True, exist_ok=True) + + self.parquet_paths = [ + parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}.parquet")) for name in parquet_names + ] + + def save_parquet( + self, + dfs: list(ddf.DataFrame | DataFrame), + parquet_paths, + drop_index=False, + ) -> None: """ Save the DataFrame to a Parquet file. Args: - df (DFType | List[DFType]): The Dask DataFrame to be saved. + dfs (DataFrame | ddf.DataFrame): The pandas or Dask Dataframe to be saved. """ + parquet_paths = parquet_paths if parquet_paths else self.parquet_paths # Compute the Dask DataFrame, reset the index, and save to Parquet - for df, parquet_paths in zip(dfs, self.parquet_paths): - df.compute().reset_index(drop=True).to_parquet(parquet_paths) - print(f"Parquet file saved: {parquet_paths}") + for df, parquet_paths in zip(dfs, parquet_paths): + df.compute().reset_index(drop=drop_index).to_parquet(parquet_paths) def read_parquet(self) -> ddf.DataFrame: """ From ff5dd07ff2fd0e869ea494b2619529450662da16 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 15:57:45 +0100 Subject: [PATCH 005/300] now uses a simpler notation and save_parquet method after loading dataframes --- sed/loader/fel/buffer.py | 68 +++++++++------------------------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index f94094c8..a42f730b 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -1,21 +1,18 @@ from __future__ import annotations +from itertools import compress from pathlib import Path -from typing import TypeVar import dask.dataframe as ddf import numpy as np import pyarrow.parquet as pq from joblib import delayed from joblib import Parallel -from pandas import DataFrame from sed.core.dfops import forward_fill_lazy from sed.loader.fel.dataframe import DataFrameCreator from sed.loader.fel.parquet import ParquetHandler -DFType = TypeVar("DFType", DataFrame, ddf.DataFrame) - class BufferFileHandler(ParquetHandler, DataFrameCreator): def __init__( @@ -39,8 +36,6 @@ def __init__( self.parallel_buffer_file_creation(h5_paths, force_recreate) - self.fill() - def schema_check(self) -> None: """ Checks the schema of the Parquet files. @@ -76,33 +71,7 @@ def schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> bool | Exception: - """ - Converts an HDF5 file to Parquet format to create a buffer file. - - This method uses `create_dataframe_per_file` method to create dataframes from individual - files within an HDF5 file. The resulting dataframe is then saved to a Parquet file. - - Args: - h5_path (Path): Path to the input HDF5 file. - parquet_path (Path): Path to the output Parquet file. - - Raises: - ValueError: If an error occurs during the conversion process. - - """ - try: - ( - self.create_dataframe_per_file(h5_path) - .reset_index(level=self.multi_index) - .to_parquet(parquet_path, index=False) - ) - except Exception as exc: # pylint: disable=broad-except - self.failed_files_error.append(f"{parquet_path}: {type(exc)} {exc}") - return exc - return None - - def parallel_buffer_file_creation(self, h5_paths: list[Path], force_recreate) -> None: + def parallel_buffer_file_creation(self, h5_paths: list[Path], force_recreate: bool) -> None: """ Parallelizes the creation of buffer files. @@ -113,35 +82,28 @@ def parallel_buffer_file_creation(self, h5_paths: list[Path], force_recreate) -> ValueError: If an error occurs during the conversion process. """ - files_to_read = [ - (h5_path, parquet_path) - for h5_path, parquet_path in zip(h5_paths, self.parquet_paths) - if force_recreate or not parquet_path.exists() + to_read = [ + force_recreate or not parquet_path.exists() for parquet_path in self.parquet_paths ] + num_files = sum(to_read) + + h5_to_read = list(compress(h5_paths, to_read)) + parquets_to_read = list(compress(self.parquet_paths, to_read)) - print(f"Reading files: {len(files_to_read)} new files of {len(h5_paths)} total.") + print(f"Reading files: {num_files} new files of {len(h5_paths)} total.") # Initialize the indices for create_buffer_file conversion self.reset_multi_index() - if len(files_to_read) > 0: - error = Parallel(n_jobs=len(files_to_read), verbose=10)( - delayed(self.create_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in files_to_read - ) - if any(error): - raise RuntimeError(f"Conversion failed for some files. {error}") - - # Raise an error if the conversion failed for any files - # TODO: merge this and the previous error trackings - if self.failed_files_error: - raise FileNotFoundError( - "Conversion failed for the following files:\n" + "\n".join(self.failed_files_error), + if num_files > 0: + dataframes = Parallel(n_jobs=num_files, verbose=10)( + delayed(self.create_dataframe_per_file)(h5_path) for h5_path in h5_to_read ) - print("All files converted successfully!") + # Save the dataframes to Parquet files + self.save_parquet(dataframes, parquets_to_read) - def get_filled_dataframes(self): + def get_filled_dataframe(self): # Read all parquet files into one dataframe using dask and reads the metadata and schema dataframe = ddf.read_parquet(self.parquet_paths, calculate_divisions=True) metadata = [pq.read_metadata(file) for file in self.parquet_paths] From 7852aaf0733e72320b5ff6a7246787a454f9d6e1 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 16:01:58 +0100 Subject: [PATCH 006/300] methods made more consistent and fixing the get_index_dataset_key --- sed/loader/fel/dataframe.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 63126d79..df65477e 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -93,11 +93,11 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: channel_config = self._config["channels"][channel] if "group_name" in channel_config: - index_key = channel_config["group_name"]["index"] + index_key = channel_config["group_name"] + "/index" if channel == "timeStamp": - dataset_key = channel_config["group_name"]["time"] + dataset_key = channel_config["group_name"] + "/time" else: - dataset_key = channel_config["group_name"]["value"] + dataset_key = channel_config["group_name"] + "/value" return index_key, dataset_key elif "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] @@ -178,7 +178,6 @@ def create_dataframe_per_pulse( np_array: np.ndarray, train_id: Series, channel: str, - channel_dict: dict, ) -> DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per pulse]. @@ -187,7 +186,6 @@ def create_dataframe_per_pulse( np_array (np.ndarray): The numpy array containing the channel data. train_id (Series): The train ID Series. channel (str): The name of the channel. - channel_dict (dict): The dictionary containing channel parameters. Returns: DataFrame: The pandas DataFrame for the channel's data. @@ -195,13 +193,15 @@ def create_dataframe_per_pulse( # Special case for auxillary channels if channel == "dldAux": # Checks the channel dictionary for correct slices and creates a multicolumn DataFrame + aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() data_frames = ( Series( (np_array[i, value] for i in train_id.index), name=key, - index=train_id, - ).to_frame() - for key, value in channel_dict["dldAuxChannels"].items() + ) + .to_frame() + .set_index(train_id) + for key, value in aux_channels ) # Multiindex set and combined dataframe returned @@ -270,7 +270,7 @@ def create_dataframe_per_channel( h5_file, channel, ) # numpy Array created - channel_dict = self._config["channels"][channel] # channel parameters + cformat = self._config["channels"][channel]["format"] # channel format # If np_array is size zero, fill with NaNs if np_array.size == 0: @@ -284,15 +284,14 @@ def create_dataframe_per_channel( ) # Electron resolved data is treated here - if channel_dict["format"] == "per_electron": + if cformat == "per_electron": # If index_per_electron is None, create it for the given file if self.index_per_electron is None: + index_pulse, array_pulse = self.create_numpy_array_per_channel(h5_file, "pulseId") self.create_multi_index_per_electron( - self.create_numpy_array_per_channel( - h5_file, - "pulseId", - self._config["ubid_offset"], - ), + index_pulse, + array_pulse, + self._config["ubid_offset"], ) # Create a DataFrame for electron-resolved data @@ -303,17 +302,16 @@ def create_dataframe_per_channel( ) # Pulse resolved data is treated here - elif channel_dict["format"] == "per_pulse": + elif cformat == "per_pulse": # Create a DataFrame for pulse-resolved data data = self.create_dataframe_per_pulse( np_array, train_id, channel, - channel_dict, ) # Train resolved data is treated here - elif channel_dict["format"] == "per_train": + elif cformat == "per_train": # Create a DataFrame for train-resolved data data = self.create_dataframe_per_train(np_array, train_id, channel) From ac9abea2cb49ae5aa9c3105abb8662d7a517f1a3 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 16:04:03 +0100 Subject: [PATCH 007/300] include steinn's proposed solution to pulse_id channel being empty --- sed/loader/fel/multiindex.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sed/loader/fel/multiindex.py b/sed/loader/fel/multiindex.py index f854b0f5..b2c24b57 100644 --- a/sed/loader/fel/multiindex.py +++ b/sed/loader/fel/multiindex.py @@ -11,6 +11,7 @@ class MultiIndexCreator: def __init__(self) -> None: self.index_per_electron: MultiIndex = None self.index_per_pulse: MultiIndex = None + # Can be extended to be alias agnostic self.multi_index = ["trainId", "pulseId", "electronId"] def reset_multi_index(self) -> None: @@ -39,6 +40,9 @@ def create_multi_index_per_electron( - It creates a MultiIndex with trainId, pulseId, and electronId as the index levels. """ + if np_array.ndim != 2: + np_array = np.empty((train_id.size, 0)) + np_array[:, :] = np.nan # Calculate macrobunches macrobunches = ( Series( From 41fd70dc18fedf0da7f98bba014949114bb34a3d Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 29 Nov 2023 16:06:27 +0100 Subject: [PATCH 008/300] include unit tests and fixtures. still many to be done. needs to move to appropriate directory --- tests/loader/flash/conftest.py | 86 +++++++ tests/loader/flash/test_dataframe_creator.py | 222 ++++++++++++++++++ tests/loader/flash/test_flash_loader.py | 199 +++++----------- tests/loader/flash/test_multiindex_creator.py | 53 +++++ 4 files changed, 419 insertions(+), 141 deletions(-) create mode 100644 tests/loader/flash/conftest.py create mode 100644 tests/loader/flash/test_dataframe_creator.py create mode 100644 tests/loader/flash/test_multiindex_creator.py diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py new file mode 100644 index 00000000..6d4779a4 --- /dev/null +++ b/tests/loader/flash/conftest.py @@ -0,0 +1,86 @@ +import os +from importlib.util import find_spec + +import h5py +import pytest + +from sed.core.config import parse_config +from sed.loader.fel import DataFrameCreator +from sed.loader.fel import MultiIndexCreator + +package_dir = os.path.dirname(find_spec("sed").origin) +config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") +H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" + + +@pytest.fixture(name="config_dataframe") +def fixture_config_file_dataframe(): + """Fixture providing a configuration file for FlashLoader tests. + + Returns: + dict: The parsed configuration file. + """ + return parse_config(config_path)["dataframe"] + + +@pytest.fixture(name="h5_file") +def fixture_h5_file(): + """Fixture providing an open h5 file. + + Returns: + h5py.File: The open h5 file. + """ + return h5py.File(os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATH}"), "r") + + +@pytest.fixture(name="gmd_channel_array") +def get_pulse_channel_from_h5(config_dataframe, h5_file): + df = DataFrameCreator(config_dataframe) + train_id, pulse_id = df.create_numpy_array_per_channel(h5_file, "gmdTunnel") + return train_id, pulse_id + + +@pytest.fixture(name="pulse_id_array") +def get_pulse_ids_from_h5(config_dataframe, h5_file): + df = DataFrameCreator(config_dataframe) + train_id, pulse_id = df.create_numpy_array_per_channel(h5_file, "pulseId") + + return train_id, pulse_id + + +@pytest.fixture(name="multiindex_electron") +def fixture_multi_index_creator(config_dataframe, pulse_id_array): + """Fixture providing a MultiIndexCreator instance. + + Returns: + MultiIndexCreator: The MultiIndexCreator instance. + """ + train_id, np_array = pulse_id_array + mc = MultiIndexCreator() + mc.create_multi_index_per_electron(train_id, np_array, config_dataframe["ubid_offset"]) + return mc.index_per_electron + + +# @pytest.fixture(name="fake_data") +# def fake_data_electron(): +# # Creating manageable fake data, but not used currently +# num_trains = 5 +# max_pulse_id = 100 +# nan_threshold = 50 +# ubid_offset = 5 +# seed = 42 +# np.random.seed(seed) +# train_ids = np.arange(1600000000, 1600000000 + num_trains) +# fake_data = [] + +# for _ in train_ids: +# pulse_ids = [] +# while len(pulse_ids) < nan_threshold: +# random_pulse_ids = np.random.choice( +# np.arange(ubid_offset, nan_threshold), size=np.random.randint(0, 10)) +# pulse_ids = np.concatenate([pulse_ids, random_pulse_ids]) + +# pulse_ids = np.concatenate([pulse_ids, np.full(max_pulse_id-len(pulse_ids), np.nan)]) + +# fake_data.append(np.sort(pulse_ids)) +# return Series(train_ids, name="trainId"), np.array(fake_data), ubid_offset diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py new file mode 100644 index 00000000..8c8ae7eb --- /dev/null +++ b/tests/loader/flash/test_dataframe_creator.py @@ -0,0 +1,222 @@ +"""Tests for DataFrameCreator functionality""" +import os +from importlib.util import find_spec + +import numpy as np +import pytest +from pandas import DataFrame +from pandas import Series + +from sed.loader.fel import DataFrameCreator + + +package_dir = os.path.dirname(find_spec("sed").origin) +config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") +H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" +# Define expected channels for each format. +ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] +PULSE_CHANNELS = [ + "sampleBias", + "tofVoltage", + "extractorVoltage", + "extractorCurrent", + "cryoTemperature", + "sampleTemperature", + "dldTimeBinSize", + "gmdTunnel", +] +TRAIN_CHANNELS = ["timeStamp", "delayStage"] +INDEX_CHANNELS = ["trainId", "pulseId", "electronId"] + + +def test_get_channels_by_format(config_dataframe): + """ + Test function to verify the 'get_channels' method in FlashLoader class for + retrieving channels based on formats and index inclusion. + """ + # Initialize the FlashLoader instance with the given config_file. + df = DataFrameCreator(config_dataframe) + + # Call get_channels method with different format options. + + # Request channels for 'per_electron' format using a list. + format_electron = df.get_channels(["per_electron"]) + + # Request channels for 'per_pulse' format using a string. + format_pulse = df.get_channels("per_pulse") + + # Request channels for 'per_train' format using a list. + format_train = df.get_channels(["per_train"]) + + # Request channels for 'all' formats using a list. + format_all = df.get_channels(["all"]) + + # Request index channels only. + format_index = df.get_channels(index=True) + + # Request 'per_electron' format and include index channels. + format_index_electron = df.get_channels(["per_electron"], index=True) + + # Request 'all' formats and include index channels. + format_all_index = df.get_channels(["all"], index=True) + + # Assert that the obtained channels match the expected channels. + assert set(ELECTRON_CHANNELS) == set(format_electron) + assert set(PULSE_CHANNELS) == set(format_pulse) + assert set(TRAIN_CHANNELS) == set(format_train) + assert set(ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set(format_all) + assert set(INDEX_CHANNELS) == set(format_index) + assert set(INDEX_CHANNELS + ELECTRON_CHANNELS) == set(format_index_electron) + assert set(INDEX_CHANNELS + ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set( + format_all_index, + ) + + +def test_invalid_channel_format(config_dataframe, h5_file): + """ + Test ValueError for an invalid channel format. + """ + config = config_dataframe + config["channels"]["dldPosX"]["format"] = "foo" + + df = DataFrameCreator(config_dataframe) + + with pytest.raises(ValueError): + print(config["channels"]["dldPosX"]["group_name"]) + df.create_dataframe_per_channel(h5_file, "dldPosX") + + +def test_group_name_not_in_h5(config_dataframe, h5_file): + """ + Test ValueError when the group_name for a channel does not exist in the H5 file. + """ + config = config_dataframe + config["channels"]["dldPosX"]["group_name"] = "foo" + df = DataFrameCreator(config) + + with pytest.raises(ValueError) as e: + df.concatenate_channels(h5_file) + + assert str(e.value.args[0]) == "The group_name for channel dldPosX does not exist." + + +def test_create_numpy_array_per_channel(config_dataframe, h5_file): + """ + Test the creation of a numpy array for a given channel. + """ + df = DataFrameCreator(config_dataframe) + channel = "dldPosX" + + train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) + print(np_array.shape) + # Check that the train_id and np_array have the correct shapes and types + assert isinstance(train_id, Series) + assert isinstance(np_array, np.ndarray) + assert train_id.name == "trainId" + assert train_id.shape[0] == np_array.shape[0] + assert np_array.shape[1] == 2048 + + +def test_create_dataframe_per_electron(config_dataframe, h5_file, multiindex_electron): + """ + Test the creation of a pandas DataFrame for a channel of type [per electron]. + """ + df = DataFrameCreator(config_dataframe) + df.index_per_electron = multiindex_electron + + channel = "dldPosX" + + train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) + # this data has no nan so size should only decrease with + result_df = df.create_dataframe_per_electron(np_array, train_id, channel) + + # Check that the values are dropped for pulseId index below 0 (ubid_offset) + # this data has no nan so size should only decrease with the dropped values + assert np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448]) + assert np.all(result_df.index.get_level_values("pulseId") >= 0) + assert isinstance(result_df, DataFrame) + + # check if the dataframe shape is correct after dropping + filtered_index = [item for item in df.index_per_electron if item[1] >= 0] + assert result_df.shape[0] == len(filtered_index) + + +def test_create_dataframe_per_pulse(config_dataframe, h5_file): + """ + Test the creation of a pandas DataFrame for a channel of type [per pulse]. + """ + df = DataFrameCreator(config_dataframe) + train_id, np_array = df.create_numpy_array_per_channel(h5_file, "gmdTunnel") + result_df = df.create_dataframe_per_pulse(np_array, train_id, "gmdTunnel") + + # Check that the result_df is a DataFrame and has the correct shape + assert isinstance(result_df, DataFrame) + assert result_df.shape[0] == np_array.shape[0] * np_array.shape[1] + + train_id, np_array = df.create_numpy_array_per_channel(h5_file, "dldAux") + result_df = df.create_dataframe_per_pulse(np_array, train_id, "dldAux") + + # Check if the subchannels are correctly sliced into the dataframe + assert isinstance(result_df, DataFrame) + assert result_df.shape[0] == len(train_id) + channel = "sampleBias" + assert np.all(result_df[channel].values == np_array[:, 0]) + + # check that dataframe contains all subchannels + assert np.all( + set(result_df.columns) + == set(config_dataframe["channels"]["dldAux"]["dldAuxChannels"].keys()), + ) + + +# def test_create_dataframe_per_train(config_dataframe, h5_file): +# """ +# Test the creation of a pandas DataFrame for a channel of type [per train]. +# """ +# df = DataFrameCreator(config_dataframe) +# channel = "timeStamp" + +# train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) +# result_df = df.create_dataframe_per_train(np_array, train_id, channel) + +# # Check that the result_df is a DataFrame and has the correct shape +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == train_id.shape[0] + + +# def test_create_dataframe_per_channel(config_dataframe, h5_file): +# """ +# Test the creation of a pandas Series or DataFrame for a channel from a given file. +# """ +# df = DataFrameCreator(config_dataframe) +# channel = "dldPosX" + +# result = df.create_dataframe_per_channel(h5_file, channel) + +# # Check that the result is a Series or DataFrame and has the correct shape +# assert isinstance(result, (Series, DataFrame)) +# assert result.shape[0] == df.create_numpy_array_per_channel(h5_file, channel)[0].shape[0] + + +# def test_concatenate_channels(config_dataframe, h5_file): +# """ +# Test the concatenation of channels from an h5py.File into a pandas DataFrame. +# """ +# df = DataFrameCreator(config_dataframe) +# result_df = df.concatenate_channels(h5_file) + +# # Check that the result_df is a DataFrame and has the correct shape +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == df.create_dataframe_per_file(Path(h5_file.filename)).shape[0] + + +# def test_create_dataframe_per_file(config_dataframe, h5_file): +# """ +# Test the creation of pandas DataFrames for a given file. +# """ +# df = DataFrameCreator(config_dataframe) +# result_df = df.create_dataframe_per_file(Path(h5_file.filename)) + +# # Check that the result_df is a DataFrame and has the correct shape +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == df.concatenate_channels(h5_file).shape[0] diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index edff997e..88ed9e8e 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -24,64 +24,6 @@ def fixture_config_file() -> dict: return parse_config(config_path) -def test_get_channels_by_format(config_file: dict) -> None: - """ - Test function to verify the 'get_channels' method in FlashLoader class for - retrieving channels based on formats and index inclusion. - """ - # Initialize the FlashLoader instance with the given config_file. - fl = FlashLoader(config_file) - - # Define expected channels for each format. - electron_channels = ["dldPosX", "dldPosY", "dldTimeSteps"] - pulse_channels = [ - "sampleBias", - "tofVoltage", - "extractorVoltage", - "extractorCurrent", - "cryoTemperature", - "sampleTemperature", - "dldTimeBinSize", - "gmdTunnel", - ] - train_channels = ["timeStamp", "delayStage"] - index_channels = ["trainId", "pulseId", "electronId"] - - # Call get_channels method with different format options. - - # Request channels for 'per_electron' format using a list. - format_electron = fl.get_channels(["per_electron"]) - - # Request channels for 'per_pulse' format using a string. - format_pulse = fl.get_channels("per_pulse") - - # Request channels for 'per_train' format using a list. - format_train = fl.get_channels(["per_train"]) - - # Request channels for 'all' formats using a list. - format_all = fl.get_channels(["all"]) - - # Request index channels only. - format_index = fl.get_channels(index=True) - - # Request 'per_electron' format and include index channels. - format_index_electron = fl.get_channels(["per_electron"], index=True) - - # Request 'all' formats and include index channels. - format_all_index = fl.get_channels(["all"], index=True) - - # Assert that the obtained channels match the expected channels. - assert set(electron_channels) == set(format_electron) - assert set(pulse_channels) == set(format_pulse) - assert set(train_channels) == set(format_train) - assert set(electron_channels + pulse_channels + train_channels) == set(format_all) - assert set(index_channels) == set(format_index) - assert set(index_channels + electron_channels) == set(format_index_electron) - assert set(index_channels + electron_channels + pulse_channels + train_channels) == set( - format_all_index, - ) - - @pytest.mark.parametrize( "sub_dir", ["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], @@ -140,86 +82,61 @@ def test_initialize_paths_filenotfound(config_file: dict) -> None: _, _ = fl.initialize_paths() -def test_invalid_channel_format(config_file: dict) -> None: - """ - Test ValueError for an invalid channel format. - """ - config = config_file - config["dataframe"]["channels"]["dldPosX"]["format"] = "foo" - - fl = FlashLoader(config=config) - - with pytest.raises(ValueError): - fl.read_dataframe() - - -def test_group_name_not_in_h5(config_file: dict) -> None: - """ - Test ValueError when the group_name for a channel does not exist in the H5 file. - """ - config = config_file - config["dataframe"]["channels"]["dldPosX"]["group_name"] = "foo" - fl = FlashLoader(config=config) - - with pytest.raises(ValueError) as e: - fl.create_dataframe_per_file(Path(config["core"]["paths"]["data_raw_dir"] + H5_PATH)) - - assert str(e.value.args[0]) == "The group_name for channel dldPosX does not exist." - - -def test_buffer_schema_mismatch(config_file: dict) -> None: - """ - Test function to verify schema mismatch handling in the FlashLoader's 'read_dataframe' method. - - The test validates the error handling mechanism when the available channels do not match the - schema of the existing parquet files. - - Test Steps: - - Attempt to read a dataframe after adding a new channel 'gmdTunnel2' to the configuration. - - Check for an expected error related to the mismatch between available channels and schema. - - Force recreation of dataframe with the added channel, ensuring successful dataframe creation. - - Simulate a missing channel scenario by removing 'gmdTunnel2' from the configuration. - - Check for an error indicating a missing channel in the configuration. - - Clean up created buffer files after the test. - """ - fl = FlashLoader(config=config_file) - - # Read a dataframe for a specific run - fl.read_dataframe(runs=["43878"]) - - # Manipulate the configuration to introduce a new channel 'gmdTunnel2' - config = config_file - config["dataframe"]["channels"]["gmdTunnel2"] = { - "group_name": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/", - "format": "per_pulse", - } - - # Reread the dataframe with the modified configuration, expecting a schema mismatch error - fl = FlashLoader(config=config) - with pytest.raises(ValueError) as e: - fl.read_dataframe(runs=["43878"]) - expected_error = e.value.args - - # Validate the specific error messages for schema mismatch - assert "The available channels do not match the schema of file" in expected_error[0] - assert expected_error[2] == "Missing in parquet: {'gmdTunnel2'}" - assert expected_error[4] == "Please check the configuration file or set force_recreate to True." - - # Force recreation of the dataframe, including the added channel 'gmdTunnel2' - fl.read_dataframe(runs=["43878"], force_recreate=True) - - # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario - del config["dataframe"]["channels"]["gmdTunnel2"] - fl = FlashLoader(config=config) - with pytest.raises(ValueError) as e: - # Attempt to read the dataframe again to check for the missing channel error - fl.read_dataframe(runs=["43878"]) - - expected_error = e.value.args - # Check for the specific error message indicating a missing channel in the configuration - assert expected_error[3] == "Missing in config: {'gmdTunnel2'}" - - # Clean up created buffer files after the test - _, parquet_data_dir = fl.initialize_paths() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) +# def test_buffer_schema_mismatch(config_file): +# """ +# Test function to verify schema mismatch handling in the FlashLoader's 'read_dataframe' method. + +# The test validates the error handling mechanism when the available channels do not match the +# schema of the existing parquet files. + +# Test Steps: +# - Attempt to read a dataframe after adding a new channel 'gmdTunnel2' to the configuration. +# - Check for an expected error related to the mismatch between available channels and schema. +# - Force recreation of dataframe with the added channel, ensuring successful dataframe +# creation. +# - Simulate a missing channel scenario by removing 'gmdTunnel2' from the configuration. +# - Check for an error indicating a missing channel in the configuration. +# - Clean up created buffer files after the test. +# """ +# fl = FlashLoader(config=config_file) + +# # Read a dataframe for a specific run +# fl.read_dataframe(runs=["43878"]) + +# # Manipulate the configuration to introduce a new channel 'gmdTunnel2' +# config = config_file +# config["dataframe"]["channels"]["gmdTunnel2"] = { +# "group_name": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/", +# "format": "per_pulse", +# } + +# # Reread the dataframe with the modified configuration, expecting a schema mismatch error +# fl = FlashLoader(config=config) +# with pytest.raises(ValueError) as e: +# fl.read_dataframe(runs=["43878"]) +# expected_error = e.value.args + +# # Validate the specific error messages for schema mismatch +# assert "The available channels do not match the schema of file" in expected_error[0] +# assert expected_error[2] == "Missing in parquet: {'gmdTunnel2'}" +# assert expected_error[4] == "Please check the configuration file or set force_recreate to +# True." + +# # Force recreation of the dataframe, including the added channel 'gmdTunnel2' +# fl.read_dataframe(runs=["43878"], force_recreate=True) + +# # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario +# del config["dataframe"]["channels"]["gmdTunnel2"] +# fl = FlashLoader(config=config) +# with pytest.raises(ValueError) as e: +# # Attempt to read the dataframe again to check for the missing channel error +# fl.read_dataframe(runs=["43878"]) + +# expected_error = e.value.args +# # Check for the specific error message indicating a missing channel in the configuration +# assert expected_error[3] == "Missing in config: {'gmdTunnel2'}" + +# # Clean up created buffer files after the test +# _, parquet_data_dir = fl.initialize_paths() +# for file in os.listdir(Path(parquet_data_dir, "buffer")): +# os.remove(Path(parquet_data_dir, "buffer", file)) diff --git a/tests/loader/flash/test_multiindex_creator.py b/tests/loader/flash/test_multiindex_creator.py new file mode 100644 index 00000000..bd2eb8ee --- /dev/null +++ b/tests/loader/flash/test_multiindex_creator.py @@ -0,0 +1,53 @@ +import numpy as np +from pandas import MultiIndex + +from sed.loader.fel import MultiIndexCreator + + +def test_reset_multi_index(): + mi = MultiIndexCreator() + mi.reset_multi_index() + assert mi.index_per_electron is None + assert mi.index_per_pulse is None + + +def test_create_multi_index_per_electron(pulse_id_array, config_dataframe): + train_id, np_array = pulse_id_array + mi = MultiIndexCreator() + mi.create_multi_index_per_electron(train_id, np_array, 5) + + # Check if the index_per_electron is a MultiIndex and has the correct levels + assert isinstance(mi.index_per_electron, MultiIndex) + assert set(mi.index_per_electron.names) == {"trainId", "pulseId", "electronId"} + + # Check if the index_per_electron has the correct number of elements + array_without_nan = np_array[~np.isnan(np_array)] + assert len(mi.index_per_electron) == array_without_nan.size + + assert np.all(mi.index_per_electron.get_level_values("trainId").unique() == train_id) + assert np.all( + mi.index_per_electron.get_level_values("pulseId").values + == array_without_nan - config_dataframe["ubid_offset"], + ) + + assert np.all( + mi.index_per_electron.get_level_values("electronId").values[:5] == [0, 1, 0, 1, 2], + ) + + assert np.all( + mi.index_per_electron.get_level_values("electronId").values[-5:] == [0, 1, 0, 1, 0], + ) + + +def test_create_multi_index_per_pulse(gmd_channel_array): + # can use pulse_id_array as it is also pulse resolved + train_id, np_array = gmd_channel_array + mi = MultiIndexCreator() + mi.create_multi_index_per_pulse(train_id, np_array) + + # Check if the index_per_pulse is a MultiIndex and has the correct levels + assert isinstance(mi.index_per_pulse, MultiIndex) + assert set(mi.index_per_pulse.names) == {"trainId", "pulseId"} + assert len(mi.index_per_pulse) == np_array.size + print(mi.index_per_pulse.get_level_values("pulseId")) + assert np.all(mi.index_per_pulse.get_level_values("pulseId").values[:7] == np.arange(0, 7)) From da00635b57338f037fcf5fcfc5604c2b2532e185 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 1 Dec 2023 08:14:03 +0100 Subject: [PATCH 009/300] add more tests, simplify logic on dataframe class --- sed/loader/fel/dataframe.py | 44 +++++----- tests/loader/flash/conftest.py | 17 ++++ tests/loader/flash/test_dataframe_creator.py | 85 ++++++++++++++------ 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index df65477e..30c17d8d 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -93,11 +93,11 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: channel_config = self._config["channels"][channel] if "group_name" in channel_config: - index_key = channel_config["group_name"] + "/index" + index_key = channel_config["group_name"] + "index" if channel == "timeStamp": - dataset_key = channel_config["group_name"] + "/time" + dataset_key = channel_config["group_name"] + "time" else: - dataset_key = channel_config["group_name"] + "/value" + dataset_key = channel_config["group_name"] + "value" return index_key, dataset_key elif "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] @@ -106,7 +106,7 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: "For channel:", channel, "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (allows only 'index' and 'value' or 'time' keys.)", + "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", ) def create_numpy_array_per_channel( @@ -128,19 +128,25 @@ def create_numpy_array_per_channel( # Get the data from the necessary h5 file and channel index_key, dataset_key = self.get_index_dataset_key(channel) - channel_dict = self._config["channels"][channel] # channel parameters + slice = self._config["channels"][channel].get("slice", None) train_id = Series(h5_file[index_key], name="trainId") # macrobunch np_array = h5_file[dataset_key][()] # Use predefined axis and slice from the json file # to choose correct dimension for necessary channel - if "slice" in channel_dict: + if slice is not None: np_array = np.take( np_array, - channel_dict["slice"], + slice, axis=1, ) + + # If np_array is size zero, fill with NaNs + if np_array.size == 0: + # Fill the np_array with NaN values of the same shape as train_id + np_array = np.full_like(train_id, np.nan, dtype=np.double) + return train_id, np_array def create_dataframe_per_electron( @@ -248,7 +254,7 @@ def create_dataframe_per_channel( self, h5_file: h5py.File, channel: str, - ) -> Series | DataFrame: + ) -> DataFrame: """ Returns a pandas DataFrame for a given channel name from a given file. @@ -261,7 +267,7 @@ def create_dataframe_per_channel( channel (str): The name of the channel. Returns: - Union[Series, DataFrame]: A pandas Series or DataFrame representing the channel's data. + Series: A pandas DataFrame representing the channel's data. Raises: ValueError: If the channel has an undefined format. @@ -272,17 +278,6 @@ def create_dataframe_per_channel( ) # numpy Array created cformat = self._config["channels"][channel]["format"] # channel format - # If np_array is size zero, fill with NaNs - if np_array.size == 0: - # Fill the np_array with NaN values of the same shape as train_id - np_array = np.full_like(train_id, np.nan, dtype=np.double) - # Create a Series using np_array, with train_id as the index - data = Series( - (np_array[i] for i in train_id.index), - name=channel, - index=train_id, - ) - # Electron resolved data is treated here if cformat == "per_electron": # If index_per_electron is None, create it for the given file @@ -347,14 +342,11 @@ def concatenate_channels( # Check for if the provided group_name actually exists in the file for channel in self._config["channels"]: - if channel == "timeStamp": - group_name = self._config["channels"][channel]["group_name"] + "time" - else: - group_name = self._config["channels"][channel]["group_name"] + "value" + index_key, dataset_key = self.get_index_dataset_key(channel) - if group_name not in all_keys: + if index_key or dataset_key not in all_keys: raise ValueError( - f"The group_name for channel {channel} does not exist.", + f"The index_key or dataset_key for channel {channel} does not exist.", ) # Create a generator expression to generate data frames for each channel diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 6d4779a4..76c8612d 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -1,4 +1,5 @@ import os +import shutil from importlib.util import find_spec import h5py @@ -33,6 +34,22 @@ def fixture_h5_file(): return h5py.File(os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATH}"), "r") +@pytest.fixture(name="h5_file_copy") +def fixture_h5_file_copy(tmp_path): + """Fixture providing a copy of an open h5 file. + + Returns: + h5py.File: The open h5 file copy. + """ + # Create a copy of the h5 file in a temporary directory + original_file_path = os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATH}") + copy_file_path = tmp_path / "copy.h5" + shutil.copyfile(original_file_path, copy_file_path) + + # Open the copy in 'read-write' mode and return it + return h5py.File(copy_file_path, "r+") + + @pytest.fixture(name="gmd_channel_array") def get_pulse_channel_from_h5(config_dataframe, h5_file): df = DataFrameCreator(config_dataframe) diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 8c8ae7eb..45752e84 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -100,10 +100,23 @@ def test_group_name_not_in_h5(config_dataframe, h5_file): assert str(e.value.args[0]) == "The group_name for channel dldPosX does not exist." +def test_get_index_dataset_key(config_dataframe): + config = config_dataframe + channel = "dldPosX" + df = DataFrameCreator(config) + index_key, dataset_key = df.get_index_dataset_key(channel) + group_name = config["channels"][channel]["group_name"] + assert index_key == group_name + "index" + assert dataset_key == group_name + "value" + + # remove group_name key + del config["channels"][channel]["group_name"] + with pytest.raises(ValueError): + df.get_index_dataset_key(channel) + + def test_create_numpy_array_per_channel(config_dataframe, h5_file): - """ - Test the creation of a numpy array for a given channel. - """ + df = DataFrameCreator(config_dataframe) channel = "dldPosX" @@ -117,6 +130,26 @@ def test_create_numpy_array_per_channel(config_dataframe, h5_file): assert np_array.shape[1] == 2048 +def test_empty_numpy_array_per_channel(config_dataframe, h5_file, h5_file_copy): + + df = DataFrameCreator(config_dataframe) + channel = "dldPosX" + channel_dataset_key = config_dataframe["channels"][channel]["group_name"] + "value" + + train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) + # alter the h5 file to have no data + h5_file_copy[channel_dataset_key][...] = np.nan + + train_id, np_array_empty_dset = df.create_numpy_array_per_channel(h5_file_copy, channel) + print(train_id.shape) + print(np_array_empty_dset.shape) + print(h5_file_copy[channel_dataset_key][()].shape) + + assert train_id.shape[0] == np_array.shape[0] + assert np_array.shape[1] == 2048 + assert np_array_empty_dset.shape[1] == np_array.shape[1] + + def test_create_dataframe_per_electron(config_dataframe, h5_file, multiindex_electron): """ Test the creation of a pandas DataFrame for a channel of type [per electron]. @@ -169,33 +202,37 @@ def test_create_dataframe_per_pulse(config_dataframe, h5_file): ) -# def test_create_dataframe_per_train(config_dataframe, h5_file): -# """ -# Test the creation of a pandas DataFrame for a channel of type [per train]. -# """ -# df = DataFrameCreator(config_dataframe) -# channel = "timeStamp" +def test_create_dataframe_per_train(config_dataframe, h5_file): + """ + Test the creation of a pandas DataFrame for a channel of type [per train]. + """ + df = DataFrameCreator(config_dataframe) + channel = "delayStage" -# train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) -# result_df = df.create_dataframe_per_train(np_array, train_id, channel) + train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) + result_df = df.create_dataframe_per_train(np_array, train_id, channel) -# # Check that the result_df is a DataFrame and has the correct shape -# assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == train_id.shape[0] + # Check that the result_df is a DataFrame and has the correct shape + assert isinstance(result_df, DataFrame) + assert result_df.shape[0] == train_id.shape[0] + assert np.all(np.equal(np.squeeze(result_df.values), np_array)) -# def test_create_dataframe_per_channel(config_dataframe, h5_file): -# """ -# Test the creation of a pandas Series or DataFrame for a channel from a given file. -# """ -# df = DataFrameCreator(config_dataframe) -# channel = "dldPosX" +@pytest.mark.parametrize( + "channel", + [ELECTRON_CHANNELS[0], "dldAux", PULSE_CHANNELS[-1], TRAIN_CHANNELS[0]], +) +def test_create_dataframe_per_channel(config_dataframe, h5_file, multiindex_electron, channel): + """ + Test the creation of a pandas Series or DataFrame for a channel from a given file. + """ + df = DataFrameCreator(config_dataframe) + df.index_per_electron = multiindex_electron -# result = df.create_dataframe_per_channel(h5_file, channel) + result = df.create_dataframe_per_channel(h5_file, channel) -# # Check that the result is a Series or DataFrame and has the correct shape -# assert isinstance(result, (Series, DataFrame)) -# assert result.shape[0] == df.create_numpy_array_per_channel(h5_file, channel)[0].shape[0] + # Check that the result is a Series or DataFrame and has the correct shape + assert isinstance(result, DataFrame) # def test_concatenate_channels(config_dataframe, h5_file): From 8b39bdb962663561ed1b9522ec830132fca58500 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 3 Dec 2023 22:08:41 +0100 Subject: [PATCH 010/300] remove the gmdTunnel channel because the datafile is not correct. Replace with pulserSignAdc --- tests/data/loader/flash/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index b7049dfb..f574bd12 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -122,9 +122,9 @@ dataframe: format: per_train group_name: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/" - gmdTunnel: + pulserSignAdc: format: per_pulse - group_name: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/" + group_name: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/" # The prefixes of the stream names for different DAQ systems for parsing filenames # (Not to be changed by user) From e1b9a9fa03599fcb314b0ef89ded5d5c8593d54c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 11 Dec 2023 19:22:19 +0100 Subject: [PATCH 011/300] major structure changes --- sed/loader/fel/dataframe.py | 372 +++++++++++++++--------------------- 1 file changed, 157 insertions(+), 215 deletions(-) diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 30c17d8d..cd027346 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -1,19 +1,19 @@ from __future__ import annotations -from functools import reduce from pathlib import Path import h5py import numpy as np +from pandas import concat from pandas import DataFrame +from pandas import Index +from pandas import MultiIndex from pandas import Series -from sed.loader.fel.multiindex import MultiIndexCreator -from sed.loader.utils import parse_h5_keys from sed.loader.utils import split_dld_time_from_sector_id -class DataFrameCreator(MultiIndexCreator): +class DataFrameCreator: """ Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ @@ -26,17 +26,17 @@ def __init__(self, config_dataframe: dict) -> None: config_dataframe (dict): The configuration dictionary with only the dataframe key. """ super().__init__() + self.h5_file: h5py.File = None self.failed_files_error: list[str] = [] + self.multi_index = ["trainId", "pulseId", "electronId"] self._config = config_dataframe - @property - def available_channels(self) -> list: - """Returns the channel names that are available for use, excluding pulseId.""" - available_channels = list(self._config["channels"].keys()) - available_channels.remove("pulseId") - return available_channels - - def get_channels(self, formats: str | list[str] = "", index: bool = False) -> list[str]: + def get_channels( + self, + formats: str | list[str] = "", + index: bool = False, + extend_aux: bool = True, + ) -> list[str]: """ Returns a list of channels associated with the specified format(s). @@ -44,17 +44,26 @@ def get_channels(self, formats: str | list[str] = "", index: bool = False) -> li formats (Union[str, List[str]]): The desired format(s) ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multi_index. + extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, + else includes 'dldAux'. Returns: List[str]: A list of channels with the specified format(s). """ + # Get the available channels excluding 'pulseId'. + available_channels = list(self._config["channels"].keys()) + available_channels.remove("pulseId") # If 'formats' is a single string, convert it to a list for uniform processing. if isinstance(formats, str): formats = [formats] # If 'formats' is a string "all", gather all possible formats. if formats == ["all"]: - channels = self.get_channels(["per_pulse", "per_train", "per_electron"], index) + channels = self.get_channels( + ["per_pulse", "per_train", "per_electron"], + index, + extend_aux, + ) return channels channels = [] @@ -62,14 +71,18 @@ def get_channels(self, formats: str | list[str] = "", index: bool = False) -> li # Gather channels based on the specified format(s). channels.extend( key - for key in self.available_channels + for key in available_channels if self._config["channels"][key]["format"] == format_ and key != "dldAux" ) - # Include 'dldAuxChannels' if the format is 'per_pulse'. - if format_ == "per_pulse": - channels.extend( - self._config["channels"]["dldAux"]["dldAuxChannels"].keys(), - ) + # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. + # Oterwise, include 'dldAux'. + if format_ == "per_train" and "dldAux" in available_channels: + if extend_aux: + channels.extend( + self._config["channels"]["dldAux"]["dldAuxChannels"].keys(), + ) + else: + channels.extend(["dldAux"]) # Include channels from multi_index if 'index' is True. if index: @@ -109,11 +122,11 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", ) - def create_numpy_array_per_channel( + def get_dataset_array( self, - h5_file: h5py.File, channel: str, - ) -> tuple[Series, np.ndarray]: + slice: bool = False, + ): """ Returns a numpy array for a given channel name for a given file. @@ -128,33 +141,38 @@ def create_numpy_array_per_channel( # Get the data from the necessary h5 file and channel index_key, dataset_key = self.get_index_dataset_key(channel) - slice = self._config["channels"][channel].get("slice", None) - - train_id = Series(h5_file[index_key], name="trainId") # macrobunch - np_array = h5_file[dataset_key][()] - - # Use predefined axis and slice from the json file - # to choose correct dimension for necessary channel - if slice is not None: - np_array = np.take( - np_array, - slice, - axis=1, - ) + key = Index(self.h5_file[index_key], name="trainId") # macrobunch + dataset = self.h5_file[dataset_key] + if slice: + slice_index = self._config["channels"][channel].get("slice", None) + if slice_index is not None: + dataset = np.take(dataset, slice_index, axis=1) # If np_array is size zero, fill with NaNs - if np_array.size == 0: + if dataset.shape[0] == 0: # Fill the np_array with NaN values of the same shape as train_id - np_array = np.full_like(train_id, np.nan, dtype=np.double) - - return train_id, np_array + dataset = np.full_like(key, np.nan, dtype=np.double) + + return key, dataset + + def pulse_index(self, offset): + index_train, dataset_pulse = self.get_dataset_array("pulseId", slice=True) + index_train_ravel = np.repeat(index_train, dataset_pulse.shape[1]) + pulse_ravel = dataset_pulse.ravel().astype(int) - offset + microbunches = MultiIndex.from_arrays((index_train_ravel, pulse_ravel)).dropna() + microbunches_sorted, indexer = microbunches.sort_values(return_indexer=True) + electron_counts = microbunches_sorted.value_counts(sort=False).values + electrons = np.concatenate([np.arange(count) for count in electron_counts]) + return ( + MultiIndex.from_arrays( + (index_train_ravel, pulse_ravel[indexer], electrons), + names=self.multi_index, + ), + indexer, + ) - def create_dataframe_per_electron( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - ) -> DataFrame: + @property + def df_electron(self) -> DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per electron]. @@ -166,25 +184,37 @@ def create_dataframe_per_electron( Returns: DataFrame: The pandas DataFrame for the channel's data. """ - return ( - Series((np_array[i] for i in train_id.index), name=channel) - .explode() - .dropna() - .to_frame() - .set_index(self.index_per_electron) - .drop( - index=np.arange(-self._config["ubid_offset"], 0), - level=1, - errors="ignore", - ) - ) + offset = self._config["ubid_offset"] + # Index + index, indexer = self.pulse_index(offset) + + # Data logic + channels = self.get_channels("per_electron") + slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] + all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + + if all_keys_same: + _, dataset = self.get_dataset_array(channels[0]) + sliced_dataset = np.take(dataset, slice_index, axis=1) + raveled_dataset = sliced_dataset.transpose(0, 2, 1).reshape( + (-1, sliced_dataset.shape[1]), + ) # transposed to flatten dataset + + dataframe = DataFrame(raveled_dataset[indexer], index=index, columns=channels) + + else: # 3x slower + series = [] + for channel in channels: + _, dataset = self.get_dataset_array(channel) + series.append(Series(dataset[()].ravel(), index=index, name=channel)) + dataframe = concat(series, axis=1) + + drop_vals = np.arange(-offset, 0) + return dataframe.dropna().drop(index=drop_vals, level="pulseId", errors="ignore") - def create_dataframe_per_pulse( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - ) -> DataFrame: + @property + def df_pulse(self) -> DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per pulse]. @@ -196,169 +226,62 @@ def create_dataframe_per_pulse( Returns: DataFrame: The pandas DataFrame for the channel's data. """ - # Special case for auxillary channels - if channel == "dldAux": - # Checks the channel dictionary for correct slices and creates a multicolumn DataFrame - aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() - data_frames = ( - Series( - (np_array[i, value] for i in train_id.index), - name=key, - ) - .to_frame() - .set_index(train_id) - for key, value in aux_channels + series = [] + channels = self.get_channels("per_pulse") + for channel in channels: + # get slice + key, dataset = self.get_dataset_array(channel, slice=True) + index = MultiIndex.from_product( + (key, np.arange(0, dataset.shape[1]), [0]), + names=self.multi_index, ) + series.append(Series(dataset[()].ravel(), index=index, name=channel)) - # Multiindex set and combined dataframe returned - data = reduce(DataFrame.combine_first, data_frames) - - # For all other pulse resolved channels - else: - # Macrobunch resolved data is exploded to a DataFrame and the MultiIndex is set - # Creates the index_per_pulse for the given channel - self.create_multi_index_per_pulse(train_id, np_array) - data = ( - Series((np_array[i] for i in train_id.index), name=channel) - .explode() - .to_frame() - .set_index(self.index_per_pulse) - ) - - return data - - def create_dataframe_per_train( - self, - np_array: np.ndarray, - train_id: Series, - channel: str, - ) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per train]. - - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - - Returns: - DataFrame: The pandas DataFrame for the channel's data. - """ - return ( - Series((np_array[i] for i in train_id.index), name=channel) - .to_frame() - .set_index(train_id) - ) - - def create_dataframe_per_channel( - self, - h5_file: h5py.File, - channel: str, - ) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name from a given file. - - This method takes an h5py.File object `h5_file` and a channel name `channel`, and returns - a pandas DataFrame containing the data for that channel from the file. The format of the - DataFrame depends on the channel's format specified in the configuration. + return concat(series, axis=1) # much faster when concatenating similarly indexed data first - Args: - h5_file (h5py.File): The h5py.File object representing the HDF5 file. - channel (str): The name of the channel. + @property + def df_train(self) -> DataFrame: - Returns: - Series: A pandas DataFrame representing the channel's data. + series = [] + channels = self.get_channels("per_train", extend_aux=False) - Raises: - ValueError: If the channel has an undefined format. - """ - [train_id, np_array] = self.create_numpy_array_per_channel( - h5_file, - channel, - ) # numpy Array created - cformat = self._config["channels"][channel]["format"] # channel format - - # Electron resolved data is treated here - if cformat == "per_electron": - # If index_per_electron is None, create it for the given file - if self.index_per_electron is None: - index_pulse, array_pulse = self.create_numpy_array_per_channel(h5_file, "pulseId") - self.create_multi_index_per_electron( - index_pulse, - array_pulse, - self._config["ubid_offset"], - ) - - # Create a DataFrame for electron-resolved data - data = self.create_dataframe_per_electron( - np_array, - train_id, - channel, - ) - - # Pulse resolved data is treated here - elif cformat == "per_pulse": - # Create a DataFrame for pulse-resolved data - data = self.create_dataframe_per_pulse( - np_array, - train_id, - channel, + for channel in channels: + key, dataset = self.get_dataset_array(channel, slice=True) + index = MultiIndex.from_product( + (key, [0], [0]), + names=self.multi_index, ) + if channel == "dldAux": + aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() + for name, slice_aux in aux_channels: + series.append(Series(dataset[: key.size, slice_aux], index, name=name)) + else: - # Train resolved data is treated here - elif cformat == "per_train": - # Create a DataFrame for train-resolved data - data = self.create_dataframe_per_train(np_array, train_id, channel) + series.append(Series(dataset, index, name=channel)) - else: - raise ValueError( - f"{channel} has an undefined format", - "Available formats are per_pulse, per_electron and per_train", - ) + return concat(series, axis=1) - return data + @property + def df(self) -> DataFrame: + # Use pd.concat to join the data frames into a single DataFrame + return concat((self.df_electron, self.df_pulse, self.df_train), axis=1) - def concatenate_channels( - self, - h5_file: h5py.File, - ) -> DataFrame: + def validate_channel_keys(self) -> None: """ - Concatenates the channels from the provided h5py.File into a pandas DataFrame. - - This method takes an h5py.File object `h5_file` and concatenates the channels present in - the file into a single pandas DataFrame. The concatenation is performed based on the - available channels specified in the configuration. + Validates if the index and dataset keys for all channels in config exist in the h5 file. Args: h5_file (h5py.File): The h5py.File object representing the HDF5 file. - Returns: - DataFrame: A concatenated pandas DataFrame containing the channels. - Raises: - ValueError: If the group_name for any channel does not exist in the file. + KeyError: If the index or dataset keys do not exist in the file. """ - all_keys = parse_h5_keys(h5_file) # Parses all channels present - - # Check for if the provided group_name actually exists in the file for channel in self._config["channels"]: index_key, dataset_key = self.get_index_dataset_key(channel) - - if index_key or dataset_key not in all_keys: - raise ValueError( - f"The index_key or dataset_key for channel {channel} does not exist.", - ) - - # Create a generator expression to generate data frames for each channel - data_frames = ( - self.create_dataframe_per_channel(h5_file, each) for each in self.available_channels - ) - - # Use the reduce function to join the data frames into a single DataFrame - return reduce( - lambda left, right: left.join(right, how="outer"), - data_frames, - ) + if index_key not in self.h5_file: + raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") + if dataset_key not in self.h5_file: + raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") def create_dataframe_per_file( self, @@ -378,11 +301,30 @@ def create_dataframe_per_file( DataFrame: pandas DataFrame """ # Loads h5 file and creates a dataframe - with h5py.File(file_path, "r") as h5_file: - self.reset_multi_index() # Reset MultiIndexes for the next file - df = self.concatenate_channels(h5_file) - df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - df = split_dld_time_from_sector_id(df, config=self._config) - return df + self.h5_file = h5py.File(file_path, "r") + # with h5py.File(file_path, "r") as h5_file: + self.validate_channel_keys() + df = self.df() + # TODO: Not sure if this should be here and not at electron df + df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + df = split_dld_time_from_sector_id(df, config=self._config) + return df.reset_index() + + +class DataVerification: + def __init__(self, df): + self.df = df + + def verify_data(self): + # Add your verification logic here + # For example, check if 'pulseId' is sorted + if not self.df["pulseId"].is_monotonic_increasing: + print("The per_electron data is not sorted correctly.") + + def correct_data(self): + # Add your correction logic here + # For example, sort the DataFrame by 'pulseId' + self.df = self.df.sort_values(by="pulseId") + print("Sorted per pulse correctly.") From cd85dfda07094358c609a7517f82ca6d8abb0e65 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 11 Dec 2023 20:00:29 +0100 Subject: [PATCH 012/300] docstrings etc --- sed/loader/fel/dataframe.py | 97 +++++++++++++++---------------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index cd027346..c4f14c67 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -75,7 +75,7 @@ def get_channels( if self._config["channels"][key]["format"] == format_ and key != "dldAux" ) # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. - # Oterwise, include 'dldAux'. + # Otherwise, include 'dldAux'. if format_ == "per_train" and "dldAux" in available_channels: if extend_aux: channels.extend( @@ -126,16 +126,16 @@ def get_dataset_array( self, channel: str, slice: bool = False, - ): + ) -> tuple[Index, np.ndarray]: """ - Returns a numpy array for a given channel name for a given file. + Returns a numpy array for a given channel name. Args: - h5_file (h5py.File): The h5py file object. channel (str): The name of the channel. + slice (bool): If True, applies slicing on the dataset. Returns: - Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array + tuple[Index, np.ndarray]: A tuple containing the train ID Index and the numpy array for the channel's data. """ # Get the data from the necessary h5 file and channel @@ -155,13 +155,26 @@ def get_dataset_array( return key, dataset - def pulse_index(self, offset): + def pulse_index(self, offset: int) -> tuple[MultiIndex, np.ndarray]: + """ + Computes the index for the 'per_electron' data. + + Args: + offset (int): The offset value. + + Returns: + tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and + the indexer. + """ index_train, dataset_pulse = self.get_dataset_array("pulseId", slice=True) index_train_ravel = np.repeat(index_train, dataset_pulse.shape[1]) pulse_ravel = dataset_pulse.ravel().astype(int) - offset microbunches = MultiIndex.from_arrays((index_train_ravel, pulse_ravel)).dropna() - microbunches_sorted, indexer = microbunches.sort_values(return_indexer=True) - electron_counts = microbunches_sorted.value_counts(sort=False).values + # Only sort if necessary + indexer = slice(None) + if not microbunches.is_monotonic_increasing: + microbunches, indexer = microbunches.sort_values(return_indexer=True) + electron_counts = microbunches.value_counts(sort=False).values electrons = np.concatenate([np.arange(count) for count in electron_counts]) return ( MultiIndex.from_arrays( @@ -176,13 +189,8 @@ def df_electron(self) -> DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per electron]. - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - Returns: - DataFrame: The pandas DataFrame for the channel's data. + DataFrame: The pandas DataFrame for the 'per_electron' channel's data. """ offset = self._config["ubid_offset"] # Index @@ -197,18 +205,15 @@ def df_electron(self) -> DataFrame: if all_keys_same: _, dataset = self.get_dataset_array(channels[0]) sliced_dataset = np.take(dataset, slice_index, axis=1) - raveled_dataset = sliced_dataset.transpose(0, 2, 1).reshape( + raveled_dataset = sliced_dataset.reshape( (-1, sliced_dataset.shape[1]), - ) # transposed to flatten dataset + ) dataframe = DataFrame(raveled_dataset[indexer], index=index, columns=channels) else: # 3x slower - series = [] - for channel in channels: - _, dataset = self.get_dataset_array(channel) - series.append(Series(dataset[()].ravel(), index=index, name=channel)) - dataframe = concat(series, axis=1) + series = {channel: self.get_dataset_array(channel)[1].ravel() for channel in channels} + dataframe = concat(series, axis=1)[indexer] drop_vals = np.arange(-offset, 0) return dataframe.dropna().drop(index=drop_vals, level="pulseId", errors="ignore") @@ -218,13 +223,8 @@ def df_pulse(self) -> DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per pulse]. - Args: - np_array (np.ndarray): The numpy array containing the channel data. - train_id (Series): The train ID Series. - channel (str): The name of the channel. - Returns: - DataFrame: The pandas DataFrame for the channel's data. + DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. """ series = [] channels = self.get_channels("per_pulse") @@ -241,7 +241,12 @@ def df_pulse(self) -> DataFrame: @property def df_train(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per train]. + Returns: + DataFrame: The pandas DataFrame for the 'per_train' channel's data. + """ series = [] channels = self.get_channels("per_train", extend_aux=False) @@ -256,13 +261,19 @@ def df_train(self) -> DataFrame: for name, slice_aux in aux_channels: series.append(Series(dataset[: key.size, slice_aux], index, name=name)) else: - series.append(Series(dataset, index, name=channel)) return concat(series, axis=1) @property def df(self) -> DataFrame: + """ + Returns a pandas DataFrame containing data from 'per_electron', 'per_pulse', + and 'per_train' channels. + + Returns: + DataFrame: The combined pandas DataFrame. + """ # Use pd.concat to join the data frames into a single DataFrame return concat((self.df_electron, self.df_pulse, self.df_train), axis=1) @@ -270,9 +281,6 @@ def validate_channel_keys(self) -> None: """ Validates if the index and dataset keys for all channels in config exist in the h5 file. - Args: - h5_file (h5py.File): The h5py.File object representing the HDF5 file. - Raises: KeyError: If the index or dataset keys do not exist in the file. """ @@ -283,17 +291,10 @@ def validate_channel_keys(self) -> None: if dataset_key not in self.h5_file: raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") - def create_dataframe_per_file( - self, - file_path: Path, - ) -> DataFrame: + def create_dataframe_per_file(self, file_path: Path) -> DataFrame: """ Create pandas DataFrames for the given file. - This method loads an HDF5 file specified by `file_path` and constructs a pandas DataFrame - from the datasets within the file. The order of datasets in the DataFrames is the opposite - of the order specified by channel names. - Args: file_path (Path): Path to the input HDF5 file. @@ -302,7 +303,6 @@ def create_dataframe_per_file( """ # Loads h5 file and creates a dataframe self.h5_file = h5py.File(file_path, "r") - # with h5py.File(file_path, "r") as h5_file: self.validate_channel_keys() df = self.df() # TODO: Not sure if this should be here and not at electron df @@ -311,20 +311,3 @@ def create_dataframe_per_file( if self._config.get("split_sector_id_from_dld_time", False): df = split_dld_time_from_sector_id(df, config=self._config) return df.reset_index() - - -class DataVerification: - def __init__(self, df): - self.df = df - - def verify_data(self): - # Add your verification logic here - # For example, check if 'pulseId' is sorted - if not self.df["pulseId"].is_monotonic_increasing: - print("The per_electron data is not sorted correctly.") - - def correct_data(self): - # Add your correction logic here - # For example, sort the DataFrame by 'pulseId' - self.df = self.df.sort_values(by="pulseId") - print("Sorted per pulse correctly.") From f6ca14e89a31c4f58de593fada96d9137ad15ac9 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 12 Dec 2023 12:53:51 +0100 Subject: [PATCH 013/300] updated buffer creation etc. tests won't work currently --- sed/loader/fel/__init__.py | 1 - sed/loader/fel/buffer.py | 137 ++++++--- sed/loader/fel/dataframe.py | 6 +- sed/loader/fel/multiindex.py | 105 ------- sed/loader/fel/parquet.py | 14 +- sed/loader/flash/loader.py | 36 ++- sed/loader/utils.py | 8 +- tests/data/loader/flash/config.yaml | 7 +- tests/loader/flash/conftest.py | 31 +- tests/loader/flash/test_dataframe_creator.py | 286 +++++++++++------- tests/loader/flash/test_multiindex_creator.py | 18 +- 11 files changed, 331 insertions(+), 318 deletions(-) delete mode 100644 sed/loader/fel/multiindex.py diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py index aaf15800..dc6bfd4d 100644 --- a/sed/loader/fel/__init__.py +++ b/sed/loader/fel/__init__.py @@ -3,7 +3,6 @@ """ from .buffer import BufferFileHandler from .dataframe import DataFrameCreator -from .multiindex import MultiIndexCreator from .parquet import ParquetHandler __all__ = [ diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index a42f730b..b49a95dd 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -4,7 +4,6 @@ from pathlib import Path import dask.dataframe as ddf -import numpy as np import pyarrow.parquet as pq from joblib import delayed from joblib import Parallel @@ -14,7 +13,12 @@ from sed.loader.fel.parquet import ParquetHandler -class BufferFileHandler(ParquetHandler, DataFrameCreator): +class BufferFileHandler(DataFrameCreator): + """ + A class for handling the creation and manipulation of buffer files using DataFrameCreator + and ParquetHandler. + """ + def __init__( self, config_dataframe: dict, @@ -23,30 +27,58 @@ def __init__( force_recreate: bool, prefix: str = "", suffix: str = "", - ): - if len(h5_paths): - raise ValueError("No data available. Probably failed reading all h5 files") + debug: bool = False, + ) -> None: + """ + Initializes the BufferFileHandler. - h5_filenames = [Path(h5_path).stem for h5_path in h5_paths] + Args: + config_dataframe (dict): The configuration dictionary with only the dataframe key. + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + force_recreate (bool): Flag to force recreation of buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + debug (bool): Flag to enable debug mode. + """ super().__init__(config_dataframe) - super().__init__(h5_filenames, folder, "buffer", prefix, suffix) + self.pq_handler = ParquetHandler( + [Path(h5_path).stem for h5_path in h5_paths], + folder, + "buffer", + prefix, + suffix, + extension="", + ) + self.buffer_paths = self.pq_handler.parquet_paths + self.h5_to_create: list[Path] = [] + self.buffer_to_create: list[Path] = [] + + self.dataframe_electron: ddf.DataFrame = None + self.dataframe_pulse: ddf.DataFrame = None if not force_recreate: self.schema_check() - self.parallel_buffer_file_creation(h5_paths, force_recreate) + self.get_files_to_read(h5_paths, force_recreate) + + if debug: + self.serial_buffer_file_creation() + else: + self.parallel_buffer_file_creation() + + self.get_filled_dataframe() def schema_check(self) -> None: """ Checks the schema of the Parquet files. Raises: - ValueError: If the schema of the Parquet files do not match the configuration. + ValueError: If the schema of the Parquet files does not match the configuration. """ - existing_parquet_filenames = [file for file in self.parquet_paths if file.exists()] - # Check if the available channels match the schema of the existing parquet files + existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set(self.get_channels(formats="all", index=True)) + config_schema = set(self.get_channels(formats="all", index=True, extend_aux=True)) if self._config.get("split_sector_id_from_dld_time", False): config_schema.add(self._config.get("sector_id_column", False)) @@ -71,46 +103,62 @@ def schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def parallel_buffer_file_creation(self, h5_paths: list[Path], force_recreate: bool) -> None: + def get_files_to_read(self, h5_paths: list[Path], force_recreate: bool) -> None: """ - Parallelizes the creation of buffer files. + Determines the list of files to read from the H5 files. Args: - h5_paths (List[Path]): List of paths to the input HDF5 files. + h5_paths (List[Path]): List of paths to H5 files. + force_recreate (bool): Flag to force recreation of buffer files. + """ + files_to_read = [ + force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths + ] - Raises: - ValueError: If an error occurs during the conversion process. + self.h5_to_create = list(compress(h5_paths, files_to_read)) + self.buffer_to_create = list(compress(self.buffer_paths, files_to_read)) + self.num_files = len(self.h5_to_create) + + print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") + def serial_buffer_file_creation(self) -> None: """ - to_read = [ - force_recreate or not parquet_path.exists() for parquet_path in self.parquet_paths - ] - num_files = sum(to_read) + Serializes the creation of buffer files. - h5_to_read = list(compress(h5_paths, to_read)) - parquets_to_read = list(compress(self.parquet_paths, to_read)) + Raises: + ValueError: If an error occurs during the conversion process. + """ + dataframes = [] + if self.num_files > 0: + dataframes = [self.create_dataframe_per_file(h5_path) for h5_path in self.h5_to_create] - print(f"Reading files: {num_files} new files of {len(h5_paths)} total.") + for df, prq in zip(dataframes, self.buffer_to_create): + df.to_parquet(prq) - # Initialize the indices for create_buffer_file conversion - self.reset_multi_index() + def parallel_buffer_file_creation(self) -> None: + """ + Parallelizes the creation of buffer files. - if num_files > 0: - dataframes = Parallel(n_jobs=num_files, verbose=10)( - delayed(self.create_dataframe_per_file)(h5_path) for h5_path in h5_to_read + Raises: + ValueError: If an error occurs during the conversion process. + """ + dataframes = [] + if self.num_files > 0: + dataframes = Parallel(n_jobs=self.num_files, verbose=10)( + delayed(self.create_dataframe_per_file)(h5_path) for h5_path in self.h5_to_create ) - # Save the dataframes to Parquet files - self.save_parquet(dataframes, parquets_to_read) + for df, prq in zip(dataframes, self.buffer_to_create): + df.to_parquet(prq) - def get_filled_dataframe(self): - # Read all parquet files into one dataframe using dask and reads the metadata and schema - dataframe = ddf.read_parquet(self.parquet_paths, calculate_divisions=True) - metadata = [pq.read_metadata(file) for file in self.parquet_paths] - # schema = [pq.read_schema(file) for file in self.parquet_paths] + def get_filled_dataframe(self) -> None: + """ + Reads all parquet files into one dataframe using dask and fills NaN values. + """ + dataframe = ddf.read_parquet(self.buffer_paths, calculate_divisions=True) + metadata = [pq.read_metadata(file) for file in self.buffer_paths] - # Channels to fill NaN values - channels: list[str] = self.get_channels(["per_pulse", "per_train"]) + channels: list[str] = self.get_channels(["per_pulse", "per_train"], extend_aux=True) overlap = min(file.num_rows for file in metadata) @@ -121,14 +169,9 @@ def get_filled_dataframe(self): before=overlap, iterations=self._config.get("forward_fill_iterations", 2), ) - # Remove the NaNs from per_electron channels - dataframe_electron = dataframe.dropna( + + self.dataframe_electron = dataframe.dropna( subset=self.get_channels(["per_electron"]), ) - dataframe_pulse = dataframe[ - self.multi_index + self.get_channels(["per_pulse", "per_train"]) - ] - dataframe_pulse = dataframe_pulse[ - (dataframe_pulse["electronId"] == 0) | (np.isnan(dataframe_pulse["electronId"])) - ] - return dataframe_electron, dataframe_pulse + self.dataframe_pulse = dataframe[self.multi_index + channels] + self.dataframe_pulse = self.dataframe_pulse[(self.dataframe_pulse["electronId"] == 0)] diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index c4f14c67..a97d9633 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -126,7 +126,7 @@ def get_dataset_array( self, channel: str, slice: bool = False, - ) -> tuple[Index, np.ndarray]: + ) -> tuple[Index, h5py.Dataset]: """ Returns a numpy array for a given channel name. @@ -135,7 +135,7 @@ def get_dataset_array( slice (bool): If True, applies slicing on the dataset. Returns: - tuple[Index, np.ndarray]: A tuple containing the train ID Index and the numpy array + tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array for the channel's data. """ # Get the data from the necessary h5 file and channel @@ -304,7 +304,7 @@ def create_dataframe_per_file(self, file_path: Path) -> DataFrame: # Loads h5 file and creates a dataframe self.h5_file = h5py.File(file_path, "r") self.validate_channel_keys() - df = self.df() + df = self.df # TODO: Not sure if this should be here and not at electron df df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) # Correct the 3-bit shift which encodes the detector ID in the 8s time diff --git a/sed/loader/fel/multiindex.py b/sed/loader/fel/multiindex.py deleted file mode 100644 index b2c24b57..00000000 --- a/sed/loader/fel/multiindex.py +++ /dev/null @@ -1,105 +0,0 @@ -import numpy as np -from pandas import MultiIndex -from pandas import Series - - -class MultiIndexCreator: - """ - Utility class for creating MultiIndex for electron and pulse resolved DataFrames. - """ - - def __init__(self) -> None: - self.index_per_electron: MultiIndex = None - self.index_per_pulse: MultiIndex = None - # Can be extended to be alias agnostic - self.multi_index = ["trainId", "pulseId", "electronId"] - - def reset_multi_index(self) -> None: - """Resets the index per pulse and electron.""" - self.index_per_electron = None - self.index_per_pulse = None - - def create_multi_index_per_electron( - self, - train_id: Series, - np_array: np.ndarray, - ubid_offset: int, - ) -> None: - """ - Creates an index per electron using pulseId for usage with the electron - resolved pandas DataFrame. - - Args: - train_id (Series): The train ID Series. - np_array (np.ndarray): The numpy array containing the pulseId data. - ubid_offset (int): The offset for adjusting pulseId. - - Notes: - - This method relies on the 'pulseId' channel to determine - the macrobunch IDs. - - It creates a MultiIndex with trainId, pulseId, and electronId - as the index levels. - """ - if np_array.ndim != 2: - np_array = np.empty((train_id.size, 0)) - np_array[:, :] = np.nan - # Calculate macrobunches - macrobunches = ( - Series( - (np_array[i] for i in train_id.index), - name="pulseId", - index=train_id, - ) - - ubid_offset - ) - - # Explode dataframe to get all microbunch values per macrobunch, - # remove NaN values and convert to type int - microbunches = macrobunches.explode().dropna().astype(int) - - # Create temporary index values - index_temp = MultiIndex.from_arrays( - (microbunches.index, microbunches.values), - names=["trainId", "pulseId"], - ) - - # Calculate electron counts per pulseId; unique preserves the order of appearance - electron_counts = index_temp.value_counts()[index_temp.unique()].values - - # Series object for indexing with electrons - electrons = ( - Series( - [np.arange(electron_counts[i]) for i in range(electron_counts.size)], - ) - .explode() - .astype(int) - ) - - # Create a pandas MultiIndex using the exploded datasets - self.index_per_electron = MultiIndex.from_arrays( - (microbunches.index, microbunches.values, electrons), - names=self.multi_index, - ) - - def create_multi_index_per_pulse( - self, - train_id: Series, - np_array: np.ndarray, - ) -> None: - """ - Creates an index per pulse using a pulse resolved channel's macrobunch ID, for usage with - the pulse resolved pandas DataFrame. - - Args: - train_id (Series): The train ID Series. - np_array (np.ndarray): The numpy array containing the pulse resolved data. - - Notes: - - This method creates a MultiIndex with trainId and pulseId as the index levels. - """ - # Create a pandas MultiIndex, useful for comparing electron and - # pulse resolved dataframes - self.index_per_pulse = MultiIndex.from_product( - (train_id, np.arange(0, np_array.shape[1])), - names=["trainId", "pulseId"], - ) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 3ce8d35a..4b6d31bf 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -3,7 +3,6 @@ from pathlib import Path import dask.dataframe as ddf -from pandas import DataFrame class ParquetHandler: @@ -21,11 +20,12 @@ class ParquetHandler: def __init__( self, - folder=None, parquet_names=None, + folder=None, subfolder=None, prefix=None, suffix=None, + extension="parquet", parquet_paths=None, ): @@ -39,15 +39,16 @@ def __init__( if parquet_paths: self.parquet_paths: Path | list[Path] = parquet_paths else: - self._initialize_paths(folder, parquet_names, subfolder, prefix, suffix) + self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) def _initialize_paths( self, - folder: Path, parquet_names: str | list[str], + folder: Path, subfolder: str = "", prefix: str = "", suffix: str = "", + extension: str = "", ) -> None: """ Create the directory for the Parquet file. @@ -57,12 +58,13 @@ def _initialize_paths( parquet_dir.mkdir(parents=True, exist_ok=True) self.parquet_paths = [ - parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}.parquet")) for name in parquet_names + parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}.{extension}")) + for name in parquet_names ] def save_parquet( self, - dfs: list(ddf.DataFrame | DataFrame), + dfs: list(ddf.DataFrame), parquet_paths, drop_index=False, ) -> None: diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index ff757209..67d8cf2c 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -2,10 +2,10 @@ This module implements the flash data loader. This loader currently supports hextof, wespe and instruments with similar structure. The raw hdf5 data is combined and saved into buffer files and loaded as a dask dataframe. -The dataframe is a amalgamation of all h5 files for a combination of runs, where the NaNs are -automatically forward filled across different files. +The dataframe is an amalgamation of all h5 files for a combination of runs, where the NaNs are +automatically forward-filled across different files. This can then be saved as a parquet for out-of-sed processing and reread back to access other -sed funtionality. +sed functionality. """ import time from pathlib import Path @@ -32,9 +32,15 @@ class FlashLoader(BaseLoader): __name__ = "flash" - supported_file_types = ["h5"] + supported_file_types = ["h5", "parquet"] def __init__(self, config: dict) -> None: + """ + Initializes the FlashLoader. + + Args: + config (dict): Configuration dictionary. + """ super().__init__(config=config) def initialize_paths(self) -> Tuple[List[Path], Path]: @@ -104,7 +110,8 @@ def get_files_from_run_id( extension: str = "h5", **kwds, ) -> List[str]: - """Returns a list of filenames for a given run located in the specified directory + """ + Returns a list of filenames for a given run located in the specified directory for the specified data acquisition (daq). Args: @@ -193,6 +200,7 @@ def read_dataframe( save_parquet: bool = False, detector: str = "", force_recreate: bool = False, + parquet_dir: str = None, **kwds, ) -> Tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -204,14 +212,15 @@ def read_dataframe( Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding files will - be located in the location provided by ``folders``. Takes precendence over + be located in the location provided by ``folders``. Takes precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. Returns: - Tuple[dd.DataFrame, dict]: A tuple containing the concatenated DataFrame and metadata. + Tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame + and metadata. Raises: ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. @@ -246,27 +255,30 @@ def read_dataframe( ftype=ftype, metadata=metadata, ) - + parquet_dir = ( + parquet_dir or data_parquet_dir + ) # if parquet_dir is None, use data_parquet_dir filename = "_".join(str(run) for run in self.runs) converted_str = "converted" if converted else "" - prq = ParquetHandler(filename, data_parquet_dir, converted_str, "run_", detector) + prq = ParquetHandler(filename, parquet_dir, converted_str, "run_", detector) # Check if load_parquet is flagged and then load the file if it exists if load_parquet: prq.read_parquet() else: - # Obtain the parquet filenames, metadata and schema from the method + # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] - bfh = BufferFileHandler( + buffer = BufferFileHandler( self._config["dataframe"], h5_paths, data_parquet_dir, force_recreate, suffix=detector, ) - df, df_timed = bfh.get_filled_dataframe() + df = buffer.df_electron() + df_timed = buffer.df_pulse() # Save the dataframe as parquet if requested if save_parquet: prq.save_parquet(df, drop_index=True) diff --git a/sed/loader/utils.py b/sed/loader/utils.py index ab3fde3a..0d26671c 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -162,7 +162,7 @@ def split_dld_time_from_sector_id( sector_id_column (str, optional): Name of the column containing the sectorID. Defaults to config["dataframe"]["sector_id_column"]. sector_id_reserved_bits (int, optional): Number of bits reserved for the - config (dict, optional): Configuration dictionary. Defaults to None. + config (dict, optional): Dataframe configuration dictionary. Defaults to None. Returns: Union[pd.DataFrame, dask.dataframe.DataFrame]: Dataframe with the new columns. @@ -170,15 +170,15 @@ def split_dld_time_from_sector_id( if tof_column is None: if config is None: raise ValueError("Either tof_column or config must be given.") - tof_column = config["dataframe"]["tof_column"] + tof_column = config["tof_column"] if sector_id_column is None: if config is None: raise ValueError("Either sector_id_column or config must be given.") - sector_id_column = config["dataframe"]["sector_id_column"] + sector_id_column = config["sector_id_column"] if sector_id_reserved_bits is None: if config is None: raise ValueError("Either sector_id_reserved_bits or config must be given.") - sector_id_reserved_bits = config["dataframe"].get("sector_id_reserved_bits", None) + sector_id_reserved_bits = config.get("sector_id_reserved_bits", None) if sector_id_reserved_bits is None: raise ValueError('No value for "sector_id_reserved_bits" found in config.') diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index f574bd12..3a8289f0 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -102,7 +102,7 @@ dataframe: # The auxillary channel has a special structure where the group further contains # a multidim structure so further aliases are defined below dldAux: - format: per_pulse + format: per_train group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" slice: 4 dldAuxChannels: @@ -126,6 +126,11 @@ dataframe: format: per_pulse group_name: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/" + gmdTunnel: + format: per_pulse + group_name: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/" + slice: 0 + # The prefixes of the stream names for different DAQ systems for parsing filenames # (Not to be changed by user) stream_name_prefixes: diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 76c8612d..6da2fe0f 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -7,11 +7,10 @@ from sed.core.config import parse_config from sed.loader.fel import DataFrameCreator -from sed.loader.fel import MultiIndexCreator package_dir = os.path.dirname(find_spec("sed").origin) config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") -H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" +H5_PATH = "FLASH1_USER3_stream_2_run44826_file13_20230324T091535.1.h5" @pytest.fixture(name="config_dataframe") @@ -50,32 +49,22 @@ def fixture_h5_file_copy(tmp_path): return h5py.File(copy_file_path, "r+") -@pytest.fixture(name="gmd_channel_array") +@pytest.fixture(name="pulserSignAdc_channel_array") def get_pulse_channel_from_h5(config_dataframe, h5_file): df = DataFrameCreator(config_dataframe) - train_id, pulse_id = df.create_numpy_array_per_channel(h5_file, "gmdTunnel") - return train_id, pulse_id - - -@pytest.fixture(name="pulse_id_array") -def get_pulse_ids_from_h5(config_dataframe, h5_file): - df = DataFrameCreator(config_dataframe) - train_id, pulse_id = df.create_numpy_array_per_channel(h5_file, "pulseId") - + df.h5_file = h5_file + train_id, pulse_id = df.get_dataset_array("pulserSignAdc") return train_id, pulse_id @pytest.fixture(name="multiindex_electron") -def fixture_multi_index_creator(config_dataframe, pulse_id_array): - """Fixture providing a MultiIndexCreator instance. +def fixture_multi_index_electron(config_dataframe, h5_file): + """Fixture providing multi index for electron resolved data""" + df = DataFrameCreator(config_dataframe) + df.h5_file = h5_file + pulse_index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) - Returns: - MultiIndexCreator: The MultiIndexCreator instance. - """ - train_id, np_array = pulse_id_array - mc = MultiIndexCreator() - mc.create_multi_index_per_electron(train_id, np_array, config_dataframe["ubid_offset"]) - return mc.index_per_electron + return pulse_index, indexer # @pytest.fixture(name="fake_data") diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 45752e84..af3df0fb 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -2,10 +2,11 @@ import os from importlib.util import find_spec +import h5py import numpy as np import pytest from pandas import DataFrame -from pandas import Series +from pandas import Index from sed.loader.fel import DataFrameCreator @@ -15,7 +16,9 @@ H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" # Define expected channels for each format. ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] -PULSE_CHANNELS = [ +PULSE_CHANNELS = ["pulserSignAdc", "gmdTunnel"] +TRAIN_CHANNELS = ["timeStamp", "delayStage", "dldAux"] +TRAIN_CHANNELS_EXTENDED = [ "sampleBias", "tofVoltage", "extractorVoltage", @@ -23,9 +26,9 @@ "cryoTemperature", "sampleTemperature", "dldTimeBinSize", - "gmdTunnel", + "timeStamp", + "delayStage", ] -TRAIN_CHANNELS = ["timeStamp", "delayStage"] INDEX_CHANNELS = ["trainId", "pulseId", "electronId"] @@ -45,8 +48,11 @@ def test_get_channels_by_format(config_dataframe): # Request channels for 'per_pulse' format using a string. format_pulse = df.get_channels("per_pulse") + # Request channels for 'per_train' format without expanding the dldAuxChannels. + format_train = df.get_channels("per_train", extend_aux=False) + # Request channels for 'per_train' format using a list. - format_train = df.get_channels(["per_train"]) + format_train_extended = df.get_channels(["per_train"]) # Request channels for 'all' formats using a list. format_all = df.get_channels(["all"]) @@ -60,44 +66,25 @@ def test_get_channels_by_format(config_dataframe): # Request 'all' formats and include index channels. format_all_index = df.get_channels(["all"], index=True) + # Request 'all' formats and include index channels and extend aux channels + format_all_index_extend_aux = df.get_channels(["all"], index=True, extend_aux=False) + # Assert that the obtained channels match the expected channels. assert set(ELECTRON_CHANNELS) == set(format_electron) - assert set(PULSE_CHANNELS) == set(format_pulse) + assert set(TRAIN_CHANNELS_EXTENDED) == set(format_train_extended) assert set(TRAIN_CHANNELS) == set(format_train) - assert set(ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set(format_all) + assert set(PULSE_CHANNELS) == set(format_pulse) + assert set(ELECTRON_CHANNELS + TRAIN_CHANNELS_EXTENDED + PULSE_CHANNELS) == set(format_all) assert set(INDEX_CHANNELS) == set(format_index) assert set(INDEX_CHANNELS + ELECTRON_CHANNELS) == set(format_index_electron) - assert set(INDEX_CHANNELS + ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set( + assert set( + INDEX_CHANNELS + ELECTRON_CHANNELS + TRAIN_CHANNELS_EXTENDED + PULSE_CHANNELS, + ) == set( format_all_index, ) - - -def test_invalid_channel_format(config_dataframe, h5_file): - """ - Test ValueError for an invalid channel format. - """ - config = config_dataframe - config["channels"]["dldPosX"]["format"] = "foo" - - df = DataFrameCreator(config_dataframe) - - with pytest.raises(ValueError): - print(config["channels"]["dldPosX"]["group_name"]) - df.create_dataframe_per_channel(h5_file, "dldPosX") - - -def test_group_name_not_in_h5(config_dataframe, h5_file): - """ - Test ValueError when the group_name for a channel does not exist in the H5 file. - """ - config = config_dataframe - config["channels"]["dldPosX"]["group_name"] = "foo" - df = DataFrameCreator(config) - - with pytest.raises(ValueError) as e: - df.concatenate_channels(h5_file) - - assert str(e.value.args[0]) == "The group_name for channel dldPosX does not exist." + assert set(INDEX_CHANNELS + ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set( + format_all_index_extend_aux, + ) def test_get_index_dataset_key(config_dataframe): @@ -115,136 +102,209 @@ def test_get_index_dataset_key(config_dataframe): df.get_index_dataset_key(channel) -def test_create_numpy_array_per_channel(config_dataframe, h5_file): +def test_get_dataset_array(config_dataframe, h5_file): df = DataFrameCreator(config_dataframe) + df.h5_file = h5_file channel = "dldPosX" - train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) - print(np_array.shape) + train_id, dset = df.get_dataset_array(channel) # Check that the train_id and np_array have the correct shapes and types - assert isinstance(train_id, Series) - assert isinstance(np_array, np.ndarray) + assert isinstance(train_id, Index) + assert isinstance(dset, h5py.Dataset) assert train_id.name == "trainId" - assert train_id.shape[0] == np_array.shape[0] - assert np_array.shape[1] == 2048 + assert train_id.shape[0] == dset.shape[0] + assert dset.shape[1] == 5 + assert dset.shape[2] == 181 + + train_id, dset = df.get_dataset_array(channel, slice=True) + assert train_id.shape[0] == dset.shape[0] + assert dset.shape[1] == 181 + channel = "gmdTunnel" + train_id, dset = df.get_dataset_array(channel, True) + assert train_id.shape[0] == dset.shape[0] + assert dset.shape[1] == 500 -def test_empty_numpy_array_per_channel(config_dataframe, h5_file, h5_file_copy): +def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): + + channel = "gmdTunnel" df = DataFrameCreator(config_dataframe) - channel = "dldPosX" - channel_dataset_key = config_dataframe["channels"][channel]["group_name"] + "value" + df.h5_file = h5_file + train_id, dset = df.get_dataset_array(channel) + + channel_index_key = config_dataframe["channels"][channel]["group_name"] + "index" + # channel_dataset_key = config_dataframe["channels"][channel]["group_name"] + "value" + empty_dataset_key = config_dataframe["channels"][channel]["group_name"] + "empty" + config_dataframe["channels"][channel]["index_key"] = channel_index_key + config_dataframe["channels"][channel]["dataset_key"] = empty_dataset_key + # Remove the 'group_name' key + del config_dataframe["channels"][channel]["group_name"] + + # create an empty dataset + empty_dataset = h5_file_copy.create_dataset( + name=empty_dataset_key, + shape=(train_id.shape[0], 0), + ) + print(empty_dataset) - train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) - # alter the h5 file to have no data - h5_file_copy[channel_dataset_key][...] = np.nan + print(h5_file_copy[empty_dataset_key]) - train_id, np_array_empty_dset = df.create_numpy_array_per_channel(h5_file_copy, channel) - print(train_id.shape) - print(np_array_empty_dset.shape) - print(h5_file_copy[channel_dataset_key][()].shape) + df = DataFrameCreator(config_dataframe) + df.h5_file = h5_file_copy + train_id, dset_empty = df.get_dataset_array(channel) - assert train_id.shape[0] == np_array.shape[0] - assert np_array.shape[1] == 2048 - assert np_array_empty_dset.shape[1] == np_array.shape[1] + assert dset_empty.shape[0] == train_id.shape[0] + assert dset.shape[1] == 8 + assert dset_empty.shape[1] == 0 -def test_create_dataframe_per_electron(config_dataframe, h5_file, multiindex_electron): +def test_df_electron(config_dataframe, h5_file, multiindex_electron): """ Test the creation of a pandas DataFrame for a channel of type [per electron]. """ df = DataFrameCreator(config_dataframe) - df.index_per_electron = multiindex_electron + df.h5_file = h5_file - channel = "dldPosX" - - train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) - # this data has no nan so size should only decrease with - result_df = df.create_dataframe_per_electron(np_array, train_id, channel) + result_df = df.df_electron # Check that the values are dropped for pulseId index below 0 (ubid_offset) # this data has no nan so size should only decrease with the dropped values + print(np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448])) + assert False assert np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448]) assert np.all(result_df.index.get_level_values("pulseId") >= 0) assert isinstance(result_df, DataFrame) # check if the dataframe shape is correct after dropping - filtered_index = [item for item in df.index_per_electron if item[1] >= 0] + filtered_index = [item for item in result_df.index if item[1] >= 0] assert result_df.shape[0] == len(filtered_index) + assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 -def test_create_dataframe_per_pulse(config_dataframe, h5_file): - """ - Test the creation of a pandas DataFrame for a channel of type [per pulse]. - """ - df = DataFrameCreator(config_dataframe) - train_id, np_array = df.create_numpy_array_per_channel(h5_file, "gmdTunnel") - result_df = df.create_dataframe_per_pulse(np_array, train_id, "gmdTunnel") - # Check that the result_df is a DataFrame and has the correct shape - assert isinstance(result_df, DataFrame) - assert result_df.shape[0] == np_array.shape[0] * np_array.shape[1] +# def test_create_dataframe_per_pulse(config_dataframe, h5_file): +# """ +# Test the creation of a pandas DataFrame for a channel of type [per pulse]. +# """ +# df = DataFrameCreator(config_dataframe) +# train_id, np_array = df.create_numpy_array_per_channel(h5_file, "pulserSignAdc") +# result_df = df.create_dataframe_per_pulse(np_array, train_id, "pulserSignAdc") - train_id, np_array = df.create_numpy_array_per_channel(h5_file, "dldAux") - result_df = df.create_dataframe_per_pulse(np_array, train_id, "dldAux") +# # Check that the result_df is a DataFrame and has the correct shape +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == np_array.shape[0] * np_array.shape[1] - # Check if the subchannels are correctly sliced into the dataframe - assert isinstance(result_df, DataFrame) - assert result_df.shape[0] == len(train_id) - channel = "sampleBias" - assert np.all(result_df[channel].values == np_array[:, 0]) - - # check that dataframe contains all subchannels - assert np.all( - set(result_df.columns) - == set(config_dataframe["channels"]["dldAux"]["dldAuxChannels"].keys()), - ) +# train_id, np_array = df.create_numpy_array_per_channel(h5_file, "dldAux") +# result_df = df.create_dataframe_per_pulse(np_array, train_id, "dldAux") +# # Check if the subchannels are correctly sliced into the dataframe +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == len(train_id) +# channel = "sampleBias" +# assert np.all(result_df[channel].values == np_array[:, 0]) -def test_create_dataframe_per_train(config_dataframe, h5_file): - """ - Test the creation of a pandas DataFrame for a channel of type [per train]. - """ - df = DataFrameCreator(config_dataframe) - channel = "delayStage" +# # check that dataframe contains all subchannels +# assert np.all( +# set(result_df.columns) +# == set(config_dataframe["channels"]["dldAux"]["dldAuxChannels"].keys()), +# ) - train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) - result_df = df.create_dataframe_per_train(np_array, train_id, channel) +# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 - # Check that the result_df is a DataFrame and has the correct shape - assert isinstance(result_df, DataFrame) - assert result_df.shape[0] == train_id.shape[0] - assert np.all(np.equal(np.squeeze(result_df.values), np_array)) +# def test_create_dataframe_per_train(config_dataframe, h5_file): +# """ +# Test the creation of a pandas DataFrame for a channel of type [per train]. +# """ +# df = DataFrameCreator(config_dataframe) +# channel = "delayStage" + +# train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) +# result_df = df.create_dataframe_per_train(np_array, train_id, channel) + +# # Check that the result_df is a DataFrame and has the correct shape +# assert isinstance(result_df, DataFrame) +# assert result_df.shape[0] == train_id.shape[0] +# assert np.all(np.equal(np.squeeze(result_df.values), np_array)) + +# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 -@pytest.mark.parametrize( - "channel", - [ELECTRON_CHANNELS[0], "dldAux", PULSE_CHANNELS[-1], TRAIN_CHANNELS[0]], -) -def test_create_dataframe_per_channel(config_dataframe, h5_file, multiindex_electron, channel): - """ - Test the creation of a pandas Series or DataFrame for a channel from a given file. - """ - df = DataFrameCreator(config_dataframe) - df.index_per_electron = multiindex_electron - result = df.create_dataframe_per_channel(h5_file, channel) +# @pytest.mark.parametrize( +# "channel", +# [ELECTRON_CHANNELS[0], "dldAux", PULSE_CHANNELS_EXTENDED[-1], TRAIN_CHANNELS[0]], +# ) +# def test_create_dataframe_per_channel(config_dataframe, h5_file, multiindex_electron, channel): +# """ +# Test the creation of a pandas Series or DataFrame for a channel from a given file. +# """ +# df = DataFrameCreator(config_dataframe) +# df.index_per_electron = multiindex_electron + +# result = df.create_dataframe_per_channel(h5_file, channel) + +# # Check that the result is a Series or DataFrame and has the correct shape +# assert isinstance(result, DataFrame) - # Check that the result is a Series or DataFrame and has the correct shape - assert isinstance(result, DataFrame) + +# def test_invalid_channel_format(config_dataframe, h5_file): +# """ +# Test ValueError for an invalid channel format. +# """ +# config = config_dataframe +# config["channels"]["dldPosX"]["format"] = "foo" + +# df = DataFrameCreator(config_dataframe) + +# with pytest.raises(ValueError): +# df.create_dataframe_per_channel(h5_file, "dldPosX") # def test_concatenate_channels(config_dataframe, h5_file): # """ # Test the concatenation of channels from an h5py.File into a pandas DataFrame. # """ + # df = DataFrameCreator(config_dataframe) +# # Take channels for different formats as they have differing lengths +# # (train_ids can also differ) +# print(df.get_channels("all", extend_aux=False)) +# df_channels_list = [df.create_dataframe_per_channel( +# h5_file, channel).index for channel in df.get_channels("all", extend_aux=False)] +# # # print all indices +# # for i, index in enumerate(df_channels_list): +# # print(df.available_channels[i], index) +# # create union of all indices and sort them +# union_index = sorted(set().union(*df_channels_list)) +# # print(union_index) + # result_df = df.concatenate_channels(h5_file) +# diff_index = result_df.index.difference(union_index) +# print(diff_index) +# print(result_df.shape) # # Check that the result_df is a DataFrame and has the correct shape # assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == df.create_dataframe_per_file(Path(h5_file.filename)).shape[0] +# assert np.all(result_df.index == union_index) + + +# def test_group_name_not_in_h5(config_dataframe, h5_file): +# """ +# Test ValueError when the group_name for a channel does not exist in the H5 file. +# """ +# channel = "dldPosX" +# config = config_dataframe +# config["channels"][channel]["group_name"] = "foo" +# index_key = "foo" + "index" +# df = DataFrameCreator(config) + +# with pytest.raises(ValueError) as e: +# df.concatenate_channels(h5_file) + +# assert str(e.value.args[0] +# ) == f"The index key: {index_key} for channel {channel} does not exist." # def test_create_dataframe_per_file(config_dataframe, h5_file): diff --git a/tests/loader/flash/test_multiindex_creator.py b/tests/loader/flash/test_multiindex_creator.py index bd2eb8ee..f39f4ec5 100644 --- a/tests/loader/flash/test_multiindex_creator.py +++ b/tests/loader/flash/test_multiindex_creator.py @@ -38,16 +38,24 @@ def test_create_multi_index_per_electron(pulse_id_array, config_dataframe): mi.index_per_electron.get_level_values("electronId").values[-5:] == [0, 1, 0, 1, 0], ) + # check if all indexes are unique + assert len(mi.index_per_electron) == len(mi.index_per_electron.unique()) -def test_create_multi_index_per_pulse(gmd_channel_array): + +def test_create_multi_index_per_pulse(pulserSignAdc_channel_array): # can use pulse_id_array as it is also pulse resolved - train_id, np_array = gmd_channel_array + train_id, np_array = pulserSignAdc_channel_array mi = MultiIndexCreator() mi.create_multi_index_per_pulse(train_id, np_array) # Check if the index_per_pulse is a MultiIndex and has the correct levels assert isinstance(mi.index_per_pulse, MultiIndex) - assert set(mi.index_per_pulse.names) == {"trainId", "pulseId"} + assert set(mi.index_per_pulse.names) == {"trainId", "pulseId", "electronId"} assert len(mi.index_per_pulse) == np_array.size - print(mi.index_per_pulse.get_level_values("pulseId")) - assert np.all(mi.index_per_pulse.get_level_values("pulseId").values[:7] == np.arange(0, 7)) + assert np.all( + mi.index_per_pulse.get_level_values("pulseId").values[790:800] == np.arange(790, 800), + ) + assert np.all( + mi.index_per_pulse.get_level_values("pulseId").values[800:810] == np.arange(0, 10), + ) + assert np.all(mi.index_per_pulse.get_level_values("electronId") == 0) From c9f1fcc9a1879f87c79fab521463f54b18ef4c66 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 12 Dec 2023 15:31:16 +0100 Subject: [PATCH 014/300] fix linting errors and comment out tests for now --- sed/loader/fel/__init__.py | 1 - sed/loader/fel/buffer.py | 18 +++- sed/loader/fel/dataframe.py | 46 +++++++--- sed/loader/fel/parquet.py | 43 ++++++--- sed/loader/flash/loader.py | 29 +++--- tests/loader/flash/conftest.py | 33 ++++--- tests/loader/flash/test_dataframe_creator.py | 92 ++++++++++++------- tests/loader/flash/test_multiindex_creator.py | 61 ------------ 8 files changed, 168 insertions(+), 155 deletions(-) delete mode 100644 tests/loader/flash/test_multiindex_creator.py diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py index dc6bfd4d..88d39d41 100644 --- a/sed/loader/fel/__init__.py +++ b/sed/loader/fel/__init__.py @@ -8,6 +8,5 @@ __all__ = [ "BufferFileHandler", "DataFrameCreator", - "MultiIndexCreator", "ParquetHandler", ] diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index b49a95dd..dca3dc79 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -1,3 +1,17 @@ +""" +The BufferFileHandler class extends the DataFrameCreator class and uses the ParquetHandler class to +manage buffer files. It provides methods for initializing paths, checking the schema of Parquet +files, determining the list of files to read, serializing and parallelizing the creation of buffer +files, and reading all Parquet files into one Dask DataFrame. + +Typical usage example: + + buffer_handler = BufferFileHandler(config_dataframe, h5_paths, folder) + +Combined DataFrames for electrons and pulses are then available as: + buffer_handler.electron_dataframe + buffer_handler.pulse_dataframe +""" from __future__ import annotations from itertools import compress @@ -128,7 +142,7 @@ def serial_buffer_file_creation(self) -> None: Raises: ValueError: If an error occurs during the conversion process. """ - dataframes = [] + dataframes: list[ddf.DataFrame] = None if self.num_files > 0: dataframes = [self.create_dataframe_per_file(h5_path) for h5_path in self.h5_to_create] @@ -142,7 +156,7 @@ def parallel_buffer_file_creation(self) -> None: Raises: ValueError: If an error occurs during the conversion process. """ - dataframes = [] + dataframes: list[ddf.DataFrame] = None if self.num_files > 0: dataframes = Parallel(n_jobs=self.num_files, verbose=10)( delayed(self.create_dataframe_per_file)(h5_path) for h5_path in self.h5_to_create diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index a97d9633..bd6425c0 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -1,3 +1,19 @@ +""" +This module contains the DataFrameCreator class which provides functionality for creating pandas +DataFrames from HDF5 files with multiple channels. + +The DataFrameCreator class provides methods for getting channels associated with specified formats, +checking if 'group_name' and converting to 'index_key' and 'dataset_key' if so, returning +a h5 dataset for a given channel name, computing the index for the 'per_electron' data, +returning a pandas DataFrame for a given channel name of type [per electron], [per pulse], +and [per train], validating if the index and dataset keys for all channels in config exist +in the h5 file, and creating pandas DataFrames for the given file. + +Typical usage example: + + df_creator = DataFrameCreator(config_dataframe) + dataframe = df_creator.create_dataframe_per_file(file_path) +""" from __future__ import annotations from pathlib import Path @@ -112,20 +128,20 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: else: dataset_key = channel_config["group_name"] + "value" return index_key, dataset_key - elif "index_key" in channel_config and "dataset_key" in channel_config: + if "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] - else: - raise ValueError( - "For channel:", - channel, - "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", - ) + + raise ValueError( + "For channel:", + channel, + "Provide either both 'index_key' and 'dataset_key'.", + "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", + ) def get_dataset_array( self, channel: str, - slice: bool = False, + slice_: bool = False, ) -> tuple[Index, h5py.Dataset]: """ Returns a numpy array for a given channel name. @@ -144,7 +160,7 @@ def get_dataset_array( key = Index(self.h5_file[index_key], name="trainId") # macrobunch dataset = self.h5_file[dataset_key] - if slice: + if slice_: slice_index = self._config["channels"][channel].get("slice", None) if slice_index is not None: dataset = np.take(dataset, slice_index, axis=1) @@ -155,7 +171,7 @@ def get_dataset_array( return key, dataset - def pulse_index(self, offset: int) -> tuple[MultiIndex, np.ndarray]: + def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: """ Computes the index for the 'per_electron' data. @@ -166,12 +182,12 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, np.ndarray]: tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and the indexer. """ - index_train, dataset_pulse = self.get_dataset_array("pulseId", slice=True) + index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) index_train_ravel = np.repeat(index_train, dataset_pulse.shape[1]) pulse_ravel = dataset_pulse.ravel().astype(int) - offset microbunches = MultiIndex.from_arrays((index_train_ravel, pulse_ravel)).dropna() # Only sort if necessary - indexer = slice(None) + indexer: slice | np.ndarray = slice(None) if not microbunches.is_monotonic_increasing: microbunches, indexer = microbunches.sort_values(return_indexer=True) electron_counts = microbunches.value_counts(sort=False).values @@ -230,7 +246,7 @@ def df_pulse(self) -> DataFrame: channels = self.get_channels("per_pulse") for channel in channels: # get slice - key, dataset = self.get_dataset_array(channel, slice=True) + key, dataset = self.get_dataset_array(channel, slice_=True) index = MultiIndex.from_product( (key, np.arange(0, dataset.shape[1]), [0]), names=self.multi_index, @@ -251,7 +267,7 @@ def df_train(self) -> DataFrame: channels = self.get_channels("per_train", extend_aux=False) for channel in channels: - key, dataset = self.get_dataset_array(channel, slice=True) + key, dataset = self.get_dataset_array(channel, slice_=True) index = MultiIndex.from_product( (key, [0], [0]), names=self.multi_index, diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 4b6d31bf..3effc106 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -1,3 +1,14 @@ +""" +The ParquetHandler class allows for saving and reading Dask DataFrames to/from Parquet files. +It also provides methods for initializing paths, deleting Parquet files, and reading Parquet +files into a Dask DataFrame. + +Typical usage example: + + parquet_handler = ParquetHandler(parquet_names='data', folder=Path('/path/to/folder')) + parquet_handler.save_parquet(df) # df is a uncomputed Dask DataFrame + data = parquet_handler.read_parquet() +""" from __future__ import annotations from pathlib import Path @@ -20,16 +31,19 @@ class ParquetHandler: def __init__( self, - parquet_names=None, - folder=None, - subfolder=None, - prefix=None, - suffix=None, - extension="parquet", - parquet_paths=None, + parquet_names: str | list[str] = None, + folder: Path = None, + subfolder: str = None, + prefix: str = None, + suffix: str = None, + extension: str = "parquet", + parquet_paths: Path = None, ): - self.parquet_paths: Path | list[Path] = None + self.parquet_paths: list[Path] = None + + if isinstance(parquet_names, str): + parquet_names = [parquet_names] if not folder and not parquet_paths: raise ValueError("Please provide folder or parquet_paths.") @@ -37,13 +51,15 @@ def __init__( raise ValueError("With folder, please provide parquet_names.") if parquet_paths: - self.parquet_paths: Path | list[Path] = parquet_paths + self.parquet_paths = ( + parquet_paths if isinstance(parquet_paths, list) else [parquet_paths] + ) else: self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) def _initialize_paths( self, - parquet_names: str | list[str], + parquet_names: list[str], folder: Path, subfolder: str = "", prefix: str = "", @@ -64,8 +80,7 @@ def _initialize_paths( def save_parquet( self, - dfs: list(ddf.DataFrame), - parquet_paths, + df: ddf.DataFrame, drop_index=False, ) -> None: """ @@ -74,10 +89,8 @@ def save_parquet( Args: dfs (DataFrame | ddf.DataFrame): The pandas or Dask Dataframe to be saved. """ - parquet_paths = parquet_paths if parquet_paths else self.parquet_paths # Compute the Dask DataFrame, reset the index, and save to Parquet - for df, parquet_paths in zip(dfs, parquet_paths): - df.compute().reset_index(drop=drop_index).to_parquet(parquet_paths) + df.compute().reset_index(drop=drop_index).to_parquet(self.parquet_paths) def read_parquet(self) -> ddf.DataFrame: """ diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 67d8cf2c..11d99edc 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -7,12 +7,11 @@ This can then be saved as a parquet for out-of-sed processing and reread back to access other sed functionality. """ +from __future__ import annotations + import time from pathlib import Path -from typing import List from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as dd from natsort import natsorted @@ -43,7 +42,7 @@ def __init__(self, config: dict) -> None: """ super().__init__(config=config) - def initialize_paths(self) -> Tuple[List[Path], Path]: + def initialize_paths(self) -> tuple[list[Path], Path]: """ Initializes the paths based on the configuration. @@ -106,10 +105,10 @@ def initialize_paths(self) -> Tuple[List[Path], Path]: def get_files_from_run_id( self, run_id: str, - folders: Union[str, Sequence[str]] = None, + folders: str | Sequence[str] = None, extension: str = "h5", **kwds, - ) -> List[str]: + ) -> list[str]: """ Returns a list of filenames for a given run located in the specified directory for the specified data acquisition (daq). @@ -142,7 +141,7 @@ def get_files_from_run_id( # Generate the file patterns to search for in the directory file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension - files: List[Path] = [] + files: list[Path] = [] # Use pathlib to search for matching files in each directory for folder in folders: files.extend( @@ -189,9 +188,9 @@ def get_elapsed_time(self, fids=None, **kwds): def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, @@ -200,9 +199,9 @@ def read_dataframe( save_parquet: bool = False, detector: str = "", force_recreate: bool = False, - parquet_dir: str = None, + parquet_dir: str | Path = None, **kwds, - ) -> Tuple[dd.DataFrame, dd.DataFrame, dict]: + ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ Read express data from the DAQ, generating a parquet in between. @@ -256,7 +255,7 @@ def read_dataframe( metadata=metadata, ) parquet_dir = ( - parquet_dir or data_parquet_dir + Path(parquet_dir) or data_parquet_dir ) # if parquet_dir is None, use data_parquet_dir filename = "_".join(str(run) for run in self.runs) converted_str = "converted" if converted else "" @@ -277,8 +276,8 @@ def read_dataframe( force_recreate, suffix=detector, ) - df = buffer.df_electron() - df_timed = buffer.df_pulse() + df = buffer.df_electron + df_timed = buffer.df_pulse # Save the dataframe as parquet if requested if save_parquet: prq.save_parquet(df, drop_index=True) diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 6da2fe0f..8bf0c881 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -1,3 +1,5 @@ +""" This module contains fixtures for the FEL module tests. +""" import os import shutil from importlib.util import find_spec @@ -6,11 +8,12 @@ import pytest from sed.core.config import parse_config -from sed.loader.fel import DataFrameCreator + +# from sed.loader.fel import DataFrameCreator package_dir = os.path.dirname(find_spec("sed").origin) config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") -H5_PATH = "FLASH1_USER3_stream_2_run44826_file13_20230324T091535.1.h5" +H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" @pytest.fixture(name="config_dataframe") @@ -49,22 +52,22 @@ def fixture_h5_file_copy(tmp_path): return h5py.File(copy_file_path, "r+") -@pytest.fixture(name="pulserSignAdc_channel_array") -def get_pulse_channel_from_h5(config_dataframe, h5_file): - df = DataFrameCreator(config_dataframe) - df.h5_file = h5_file - train_id, pulse_id = df.get_dataset_array("pulserSignAdc") - return train_id, pulse_id +# @pytest.fixture(name="pulserSignAdc_channel_array") +# def get_pulse_channel_from_h5(config_dataframe, h5_file): +# df = DataFrameCreator(config_dataframe) +# df.h5_file = h5_file +# train_id, pulse_id = df.get_dataset_array("pulserSignAdc") +# return train_id, pulse_id -@pytest.fixture(name="multiindex_electron") -def fixture_multi_index_electron(config_dataframe, h5_file): - """Fixture providing multi index for electron resolved data""" - df = DataFrameCreator(config_dataframe) - df.h5_file = h5_file - pulse_index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) +# @pytest.fixture(name="multiindex_electron") +# def fixture_multi_index_electron(config_dataframe, h5_file): +# """Fixture providing multi index for electron resolved data""" +# df = DataFrameCreator(config_dataframe) +# df.h5_file = h5_file +# pulse_index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) - return pulse_index, indexer +# return pulse_index, indexer # @pytest.fixture(name="fake_data") diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index af3df0fb..3c569554 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -3,9 +3,7 @@ from importlib.util import find_spec import h5py -import numpy as np import pytest -from pandas import DataFrame from pandas import Index from sed.loader.fel import DataFrameCreator @@ -88,6 +86,7 @@ def test_get_channels_by_format(config_dataframe): def test_get_index_dataset_key(config_dataframe): + """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" df = DataFrameCreator(config) @@ -103,6 +102,7 @@ def test_get_index_dataset_key(config_dataframe): def test_get_dataset_array(config_dataframe, h5_file): + """Test the creation of a h5py dataset for a given channel.""" df = DataFrameCreator(config_dataframe) df.h5_file = h5_file @@ -115,19 +115,21 @@ def test_get_dataset_array(config_dataframe, h5_file): assert train_id.name == "trainId" assert train_id.shape[0] == dset.shape[0] assert dset.shape[1] == 5 - assert dset.shape[2] == 181 + assert dset.shape[2] == 2048 - train_id, dset = df.get_dataset_array(channel, slice=True) + train_id, dset = df.get_dataset_array(channel, slice_=True) assert train_id.shape[0] == dset.shape[0] - assert dset.shape[1] == 181 + assert dset.shape[1] == 2048 - channel = "gmdTunnel" - train_id, dset = df.get_dataset_array(channel, True) - assert train_id.shape[0] == dset.shape[0] - assert dset.shape[1] == 500 + # The data in file is not representative of the actual data. TODO: fix this + # channel = "gmdTunnel" + # train_id, dset = df.get_dataset_array(channel, True) + # assert train_id.shape[0] == dset.shape[0] + # assert dset.shape[1] == 1 def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): + """Test the method when given an empty dataset.""" channel = "gmdTunnel" df = DataFrameCreator(config_dataframe) @@ -143,13 +145,10 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): del config_dataframe["channels"][channel]["group_name"] # create an empty dataset - empty_dataset = h5_file_copy.create_dataset( + h5_file_copy.create_dataset( name=empty_dataset_key, shape=(train_id.shape[0], 0), ) - print(empty_dataset) - - print(h5_file_copy[empty_dataset_key]) df = DataFrameCreator(config_dataframe) df.h5_file = h5_file_copy @@ -160,28 +159,59 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): assert dset_empty.shape[1] == 0 -def test_df_electron(config_dataframe, h5_file, multiindex_electron): - """ - Test the creation of a pandas DataFrame for a channel of type [per electron]. - """ - df = DataFrameCreator(config_dataframe) - df.h5_file = h5_file +# def test_create_multi_index_per_electron(pulse_id_array, config_dataframe): +# train_id, np_array = pulse_id_array +# mi = MultiIndexCreator() +# mi.create_multi_index_per_electron(train_id, np_array, 5) + +# # Check if the index_per_electron is a MultiIndex and has the correct levels +# assert isinstance(mi.index_per_electron, MultiIndex) +# assert set(mi.index_per_electron.names) == {"trainId", "pulseId", "electronId"} + +# # Check if the index_per_electron has the correct number of elements +# array_without_nan = np_array[~np.isnan(np_array)] +# assert len(mi.index_per_electron) == array_without_nan.size + +# assert np.all(mi.index_per_electron.get_level_values("trainId").unique() == train_id) +# assert np.all( +# mi.index_per_electron.get_level_values("pulseId").values +# == array_without_nan - config_dataframe["ubid_offset"], +# ) + +# assert np.all( +# mi.index_per_electron.get_level_values("electronId").values[:5] == [0, 1, 0, 1, 2], +# ) - result_df = df.df_electron +# assert np.all( +# mi.index_per_electron.get_level_values("electronId").values[-5:] == [0, 1, 0, 1, 0], +# ) - # Check that the values are dropped for pulseId index below 0 (ubid_offset) - # this data has no nan so size should only decrease with the dropped values - print(np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448])) - assert False - assert np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448]) - assert np.all(result_df.index.get_level_values("pulseId") >= 0) - assert isinstance(result_df, DataFrame) +# # check if all indexes are unique +# assert len(mi.index_per_electron) == len(mi.index_per_electron.unique()) - # check if the dataframe shape is correct after dropping - filtered_index = [item for item in result_df.index if item[1] >= 0] - assert result_df.shape[0] == len(filtered_index) - assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 +# def test_df_electron(config_dataframe, h5_file, multiindex_electron): +# """ +# Test the creation of a pandas DataFrame for a channel of type [per electron]. +# """ +# df = DataFrameCreator(config_dataframe) +# df.h5_file = h5_file + +# result_df = df.df_electron + +# # Check that the values are dropped for pulseId index below 0 (ubid_offset) +# # this data has no nan so size should only decrease with the dropped values +# print(np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448])) +# assert False +# assert np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448]) +# assert np.all(result_df.index.get_level_values("pulseId") >= 0) +# assert isinstance(result_df, DataFrame) + +# # check if the dataframe shape is correct after dropping +# filtered_index = [item for item in result_df.index if item[1] >= 0] +# assert result_df.shape[0] == len(filtered_index) + +# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 # def test_create_dataframe_per_pulse(config_dataframe, h5_file): diff --git a/tests/loader/flash/test_multiindex_creator.py b/tests/loader/flash/test_multiindex_creator.py deleted file mode 100644 index f39f4ec5..00000000 --- a/tests/loader/flash/test_multiindex_creator.py +++ /dev/null @@ -1,61 +0,0 @@ -import numpy as np -from pandas import MultiIndex - -from sed.loader.fel import MultiIndexCreator - - -def test_reset_multi_index(): - mi = MultiIndexCreator() - mi.reset_multi_index() - assert mi.index_per_electron is None - assert mi.index_per_pulse is None - - -def test_create_multi_index_per_electron(pulse_id_array, config_dataframe): - train_id, np_array = pulse_id_array - mi = MultiIndexCreator() - mi.create_multi_index_per_electron(train_id, np_array, 5) - - # Check if the index_per_electron is a MultiIndex and has the correct levels - assert isinstance(mi.index_per_electron, MultiIndex) - assert set(mi.index_per_electron.names) == {"trainId", "pulseId", "electronId"} - - # Check if the index_per_electron has the correct number of elements - array_without_nan = np_array[~np.isnan(np_array)] - assert len(mi.index_per_electron) == array_without_nan.size - - assert np.all(mi.index_per_electron.get_level_values("trainId").unique() == train_id) - assert np.all( - mi.index_per_electron.get_level_values("pulseId").values - == array_without_nan - config_dataframe["ubid_offset"], - ) - - assert np.all( - mi.index_per_electron.get_level_values("electronId").values[:5] == [0, 1, 0, 1, 2], - ) - - assert np.all( - mi.index_per_electron.get_level_values("electronId").values[-5:] == [0, 1, 0, 1, 0], - ) - - # check if all indexes are unique - assert len(mi.index_per_electron) == len(mi.index_per_electron.unique()) - - -def test_create_multi_index_per_pulse(pulserSignAdc_channel_array): - # can use pulse_id_array as it is also pulse resolved - train_id, np_array = pulserSignAdc_channel_array - mi = MultiIndexCreator() - mi.create_multi_index_per_pulse(train_id, np_array) - - # Check if the index_per_pulse is a MultiIndex and has the correct levels - assert isinstance(mi.index_per_pulse, MultiIndex) - assert set(mi.index_per_pulse.names) == {"trainId", "pulseId", "electronId"} - assert len(mi.index_per_pulse) == np_array.size - assert np.all( - mi.index_per_pulse.get_level_values("pulseId").values[790:800] == np.arange(790, 800), - ) - assert np.all( - mi.index_per_pulse.get_level_values("pulseId").values[800:810] == np.arange(0, 10), - ) - assert np.all(mi.index_per_pulse.get_level_values("electronId") == 0) From 1398bf2e8f6e8be6c2733eedb60ace2b595e10b6 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 13 Dec 2023 16:10:22 +0100 Subject: [PATCH 015/300] fix the error of getting wrong attribute in loader, and fix parquet loading/creation --- sed/loader/fel/buffer.py | 8 ++++---- sed/loader/fel/dataframe.py | 1 + sed/loader/fel/parquet.py | 5 +++-- sed/loader/flash/loader.py | 32 ++++++++++++++++++++++---------- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index dca3dc79..8ef9723e 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -146,8 +146,8 @@ def serial_buffer_file_creation(self) -> None: if self.num_files > 0: dataframes = [self.create_dataframe_per_file(h5_path) for h5_path in self.h5_to_create] - for df, prq in zip(dataframes, self.buffer_to_create): - df.to_parquet(prq) + for df, prq in zip(dataframes, self.buffer_to_create): + df.to_parquet(prq) def parallel_buffer_file_creation(self) -> None: """ @@ -162,8 +162,8 @@ def parallel_buffer_file_creation(self) -> None: delayed(self.create_dataframe_per_file)(h5_path) for h5_path in self.h5_to_create ) - for df, prq in zip(dataframes, self.buffer_to_create): - df.to_parquet(prq) + for df, prq in zip(dataframes, self.buffer_to_create): + df.to_parquet(prq) def get_filled_dataframe(self) -> None: """ diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index bd6425c0..39205f80 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -68,6 +68,7 @@ def get_channels( """ # Get the available channels excluding 'pulseId'. available_channels = list(self._config["channels"].keys()) + # raises error if not available, but necessary for pulse_index available_channels.remove("pulseId") # If 'formats' is a single string, convert it to a list for uniform processing. if isinstance(formats, str): diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 3effc106..f15bef2d 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -80,7 +80,7 @@ def _initialize_paths( def save_parquet( self, - df: ddf.DataFrame, + dfs: list(ddf.DataFrame), drop_index=False, ) -> None: """ @@ -90,7 +90,8 @@ def save_parquet( dfs (DataFrame | ddf.DataFrame): The pandas or Dask Dataframe to be saved. """ # Compute the Dask DataFrame, reset the index, and save to Parquet - df.compute().reset_index(drop=drop_index).to_parquet(self.parquet_paths) + for df, parquet_path in zip(dfs, self.parquet_paths): + df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) def read_parquet(self) -> ddf.DataFrame: """ diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 11d99edc..08ee4856 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -31,7 +31,7 @@ class FlashLoader(BaseLoader): __name__ = "flash" - supported_file_types = ["h5", "parquet"] + supported_file_types = ["h5"] def __init__(self, config: dict) -> None: """ @@ -200,6 +200,7 @@ def read_dataframe( detector: str = "", force_recreate: bool = False, parquet_dir: str | Path = None, + debug: bool = False, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -254,17 +255,26 @@ def read_dataframe( ftype=ftype, metadata=metadata, ) - parquet_dir = ( - Path(parquet_dir) or data_parquet_dir - ) # if parquet_dir is None, use data_parquet_dir + + # if parquet_dir is None, use data_parquet_dir + parquet_dir = parquet_dir or data_parquet_dir + parquet_path = Path(parquet_dir) filename = "_".join(str(run) for run in self.runs) converted_str = "converted" if converted else "" - prq = ParquetHandler(filename, parquet_dir, converted_str, "run_", detector) + # Create parquet paths for saving and loading the parquet files of df and timed_df + prq = ParquetHandler( + [filename, filename + "_timed"], + parquet_path, + converted_str, + "run_", + detector, + ) # Check if load_parquet is flagged and then load the file if it exists if load_parquet: - prq.read_parquet() + df, df_timed = prq.read_parquet() + # Default behavior is to create the buffer files and load them else: # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading @@ -272,15 +282,17 @@ def read_dataframe( buffer = BufferFileHandler( self._config["dataframe"], h5_paths, - data_parquet_dir, + parquet_path, force_recreate, suffix=detector, + debug=debug, ) - df = buffer.df_electron - df_timed = buffer.df_pulse + df = buffer.dataframe_electron + df_timed = buffer.dataframe_pulse + # Save the dataframe as parquet if requested if save_parquet: - prq.save_parquet(df, drop_index=True) + prq.save_parquet([df, df_timed], drop_index=True) metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") From eb72230a1b86368d9fa1a14e7009dd1b90091fed Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 13 Dec 2023 16:25:09 +0100 Subject: [PATCH 016/300] fix lint error --- sed/loader/fel/parquet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index f15bef2d..2af16b20 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -80,7 +80,7 @@ def _initialize_paths( def save_parquet( self, - dfs: list(ddf.DataFrame), + dfs: list[ddf.DataFrame], drop_index=False, ) -> None: """ From 4d950dbb42a8e3f4ae10406b458ea4729bd173ea Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jan 2024 21:29:08 +0100 Subject: [PATCH 017/300] cleaning up the classes --- sed/loader/fel/__init__.py | 14 ++--- sed/loader/fel/buffer.py | 77 ++++++++++++------------ sed/loader/fel/dataframe.py | 114 ++++++------------------------------ sed/loader/fel/parquet.py | 8 --- sed/loader/fel/utils.py | 79 +++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 sed/loader/fel/utils.py diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py index 88d39d41..ae3fb46d 100644 --- a/sed/loader/fel/__init__.py +++ b/sed/loader/fel/__init__.py @@ -1,12 +1,12 @@ -"""sed.loader.fel module easy access APIs - -""" -from .buffer import BufferFileHandler +# """sed.loader.fel module easy access APIs +# """ +# from .buffer import BufferFileHandler from .dataframe import DataFrameCreator -from .parquet import ParquetHandler + +# from .parquet import ParquetHandler __all__ = [ - "BufferFileHandler", + # "BufferFileHandler", "DataFrameCreator", - "ParquetHandler", + # "ParquetHandler", ] diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 8ef9723e..3631e991 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -25,9 +25,11 @@ from sed.core.dfops import forward_fill_lazy from sed.loader.fel.dataframe import DataFrameCreator from sed.loader.fel.parquet import ParquetHandler +from sed.loader.fel.utils import get_channels +from sed.loader.utils import split_dld_time_from_sector_id -class BufferFileHandler(DataFrameCreator): +class BufferFileHandler: """ A class for handling the creation and manipulation of buffer files using DataFrameCreator and ParquetHandler. @@ -76,10 +78,7 @@ def __init__( self.get_files_to_read(h5_paths, force_recreate) - if debug: - self.serial_buffer_file_creation() - else: - self.parallel_buffer_file_creation() + self.create_buffer_files(debug) self.get_filled_dataframe() @@ -92,7 +91,10 @@ def schema_check(self) -> None: """ existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set(self.get_channels(formats="all", index=True, extend_aux=True)) + config_schema = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + if self._config.get("split_sector_id_from_dld_time", False): config_schema.add(self._config.get("sector_id_column", False)) @@ -135,35 +137,26 @@ def get_files_to_read(self, h5_paths: list[Path], force_recreate: bool) -> None: print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") - def serial_buffer_file_creation(self) -> None: - """ - Serializes the creation of buffer files. - - Raises: - ValueError: If an error occurs during the conversion process. - """ - dataframes: list[ddf.DataFrame] = None - if self.num_files > 0: - dataframes = [self.create_dataframe_per_file(h5_path) for h5_path in self.h5_to_create] - - for df, prq in zip(dataframes, self.buffer_to_create): - df.to_parquet(prq) - - def parallel_buffer_file_creation(self) -> None: + def create_buffer_files(self, debug: bool) -> None: """ - Parallelizes the creation of buffer files. + Creates the buffer files. - Raises: - ValueError: If an error occurs during the conversion process. + Args: + debug (bool): Flag to enable debug mode, which serializes the creation. """ - dataframes: list[ddf.DataFrame] = None + dataframes: list[ddf.DataFrame] = [] if self.num_files > 0: + df_creator_instances = [ + DataFrameCreator(self._config, h5_path) for h5_path in self.h5_to_create + ] + if debug: + dataframes = [df_creator.df for df_creator in df_creator_instances] + else: dataframes = Parallel(n_jobs=self.num_files, verbose=10)( - delayed(self.create_dataframe_per_file)(h5_path) for h5_path in self.h5_to_create + delayed(df_creator.df) for df_creator in df_creator_instances ) - - for df, prq in zip(dataframes, self.buffer_to_create): - df.to_parquet(prq) + for df, prq in zip(dataframes, self.buffer_to_create): + df.to_parquet(prq) def get_filled_dataframe(self) -> None: """ @@ -172,8 +165,12 @@ def get_filled_dataframe(self) -> None: dataframe = ddf.read_parquet(self.buffer_paths, calculate_divisions=True) metadata = [pq.read_metadata(file) for file in self.buffer_paths] - channels: list[str] = self.get_channels(["per_pulse", "per_train"], extend_aux=True) - + channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + index: list[str] = get_channels(index=True) overlap = min(file.num_rows for file in metadata) print("Filling nan values...") @@ -184,8 +181,16 @@ def get_filled_dataframe(self) -> None: iterations=self._config.get("forward_fill_iterations", 2), ) - self.dataframe_electron = dataframe.dropna( - subset=self.get_channels(["per_electron"]), - ) - self.dataframe_pulse = dataframe[self.multi_index + channels] - self.dataframe_pulse = self.dataframe_pulse[(self.dataframe_pulse["electronId"] == 0)] + # Drop rows with nan values in the tof column + tof_column = self._config.get("tof_column", "dldTimeSteps") + dataframe_electron = dataframe.dropna(subset=self._config.get(tof_column)) + + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + self.dataframe_electron = split_dld_time_from_sector_id( + dataframe_electron, + config=self._config, + ) + else: + self.dataframe_electron = dataframe_electron + self.dataframe_pulse = dataframe[index + channels] diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 39205f80..a414a13e 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -26,7 +26,7 @@ from pandas import MultiIndex from pandas import Series -from sed.loader.utils import split_dld_time_from_sector_id +from sed.loader.fel.utils import get_channels class DataFrameCreator: @@ -34,79 +34,18 @@ class DataFrameCreator: Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ - def __init__(self, config_dataframe: dict) -> None: + def __init__(self, config_dataframe: dict, h5_path: Path) -> None: """ Initializes the DataFrameCreator class. Args: config_dataframe (dict): The configuration dictionary with only the dataframe key. """ - super().__init__() - self.h5_file: h5py.File = None + self.h5_file: h5py.File = h5py.File(h5_path, "r") self.failed_files_error: list[str] = [] - self.multi_index = ["trainId", "pulseId", "electronId"] + self.multi_index = get_channels(index=True) self._config = config_dataframe - def get_channels( - self, - formats: str | list[str] = "", - index: bool = False, - extend_aux: bool = True, - ) -> list[str]: - """ - Returns a list of channels associated with the specified format(s). - - Args: - formats (Union[str, List[str]]): The desired format(s) - ('per_pulse', 'per_electron', 'per_train', 'all'). - index (bool): If True, includes channels from the multi_index. - extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, - else includes 'dldAux'. - - Returns: - List[str]: A list of channels with the specified format(s). - """ - # Get the available channels excluding 'pulseId'. - available_channels = list(self._config["channels"].keys()) - # raises error if not available, but necessary for pulse_index - available_channels.remove("pulseId") - # If 'formats' is a single string, convert it to a list for uniform processing. - if isinstance(formats, str): - formats = [formats] - - # If 'formats' is a string "all", gather all possible formats. - if formats == ["all"]: - channels = self.get_channels( - ["per_pulse", "per_train", "per_electron"], - index, - extend_aux, - ) - return channels - - channels = [] - for format_ in formats: - # Gather channels based on the specified format(s). - channels.extend( - key - for key in available_channels - if self._config["channels"][key]["format"] == format_ and key != "dldAux" - ) - # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. - # Otherwise, include 'dldAux'. - if format_ == "per_train" and "dldAux" in available_channels: - if extend_aux: - channels.extend( - self._config["channels"]["dldAux"]["dldAuxChannels"].keys(), - ) - else: - channels.extend(["dldAux"]) - - # Include channels from multi_index if 'index' is True. - if index: - channels.extend(self.multi_index) - - return channels - def get_index_dataset_key(self, channel: str) -> tuple[str, str]: """ Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. @@ -214,7 +153,7 @@ def df_electron(self) -> DataFrame: index, indexer = self.pulse_index(offset) # Data logic - channels = self.get_channels("per_electron") + channels = get_channels(self._config["channels"], "per_electron") slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) @@ -244,7 +183,7 @@ def df_pulse(self) -> DataFrame: DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. """ series = [] - channels = self.get_channels("per_pulse") + channels = get_channels(self._config["channels"], "per_pulse") for channel in channels: # get slice key, dataset = self.get_dataset_array(channel, slice_=True) @@ -265,7 +204,8 @@ def df_train(self) -> DataFrame: DataFrame: The pandas DataFrame for the 'per_train' channel's data. """ series = [] - channels = self.get_channels("per_train", extend_aux=False) + + channels = get_channels(self._config["channels"], "per_train") for channel in channels: key, dataset = self.get_dataset_array(channel, slice_=True) @@ -282,18 +222,6 @@ def df_train(self) -> DataFrame: return concat(series, axis=1) - @property - def df(self) -> DataFrame: - """ - Returns a pandas DataFrame containing data from 'per_electron', 'per_pulse', - and 'per_train' channels. - - Returns: - DataFrame: The combined pandas DataFrame. - """ - # Use pd.concat to join the data frames into a single DataFrame - return concat((self.df_electron, self.df_pulse, self.df_train), axis=1) - def validate_channel_keys(self) -> None: """ Validates if the index and dataset keys for all channels in config exist in the h5 file. @@ -308,23 +236,19 @@ def validate_channel_keys(self) -> None: if dataset_key not in self.h5_file: raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") - def create_dataframe_per_file(self, file_path: Path) -> DataFrame: + @property + def df(self) -> DataFrame: """ - Create pandas DataFrames for the given file. - - Args: - file_path (Path): Path to the input HDF5 file. + Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, + returning a single dataframe. Returns: - DataFrame: pandas DataFrame + DataFrame: The combined pandas DataFrame. """ - # Loads h5 file and creates a dataframe - self.h5_file = h5py.File(file_path, "r") + self.validate_channel_keys() - df = self.df - # TODO: Not sure if this should be here and not at electron df - df = df.dropna(subset=self._config.get("tof_column", "dldTimeSteps")) - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - df = split_dld_time_from_sector_id(df, config=self._config) - return df.reset_index() + return ( + self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") + .join(self.df_train, on=self.multi_index, how="outer") + .sort_index() + ) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 2af16b20..c63e53be 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -110,11 +110,3 @@ def read_parquet(self) -> ddf.DataFrame: "The Parquet file does not exist. " "If it is in another location, provide the correct path as parquet_path.", ) from exc - - def delete_parquet(self) -> None: - """ - Delete the Parquet file. - """ - for parquet_path in self.parquet_paths: - parquet_path.unlink() - print(f"Parquet file deleted: {parquet_path}") diff --git a/sed/loader/fel/utils.py b/sed/loader/fel/utils.py new file mode 100644 index 00000000..153ba642 --- /dev/null +++ b/sed/loader/fel/utils.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +MULTI_INDEX = ["trainId", "pulseId", "electronId"] +PULSE_ALIAS = MULTI_INDEX[1] +DLD_AUX_ALIAS = "dldAux" +DLDAUX_CHANNELS = "dldAuxChannels" + + +def get_channels( + channel_dict: dict = None, + formats: str | list[str] = None, + index: bool = False, + extend_aux: bool = False, +) -> list[str]: + """ + Returns a list of channels associated with the specified format(s). + + Args: + formats (Union[str, List[str]]): The desired format(s) + ('per_pulse', 'per_electron', 'per_train', 'all'). + index (bool): If True, includes channels from the multiindex. + extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, + else includes 'dldAux'. + + Returns: + List[str]: A list of channels with the specified format(s). + """ + # If 'formats' is a single string, convert it to a list for uniform processing. + if isinstance(formats, str): + formats = [formats] + + # If 'formats' is a string "all", gather all possible formats. + if formats == ["all"]: + channels = get_channels( + channel_dict, + ["per_electron", "per_pulse", "per_train"], + index, + extend_aux, + ) + return channels + + channels = [] + + # Include channels from multi_index if 'index' is True. + if index: + channels.extend(MULTI_INDEX) + + if formats: + # If 'formats' is a list, check if all elements are valid. + for format_ in formats: + if format_ not in ["per_electron", "per_pulse", "per_train", "all"]: + raise ValueError( + "Invalid format. Please choose from 'per_electron', 'per_pulse',\ + 'per_train', 'all'.", + ) + + # Get the available channels excluding 'pulseId'. + available_channels = list(channel_dict.keys()) + # raises error if not available, but necessary for pulse_index + available_channels.remove(PULSE_ALIAS) + + for format_ in formats: + # Gather channels based on the specified format(s). + channels.extend( + key + for key in available_channels + if channel_dict[key]["format"] == format_ and key != DLD_AUX_ALIAS + ) + # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. + # Otherwise, include 'dldAux'. + if format_ == "per_train" and DLD_AUX_ALIAS in available_channels: + if extend_aux: + channels.extend( + channel_dict[DLD_AUX_ALIAS][DLDAUX_CHANNELS].keys(), + ) + else: + channels.extend([DLD_AUX_ALIAS]) + + return channels From b8bfdf049f2780c8aafd7f7f18dcd26d22a22c34 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jan 2024 21:36:28 +0100 Subject: [PATCH 018/300] add back easy access apis --- sed/loader/fel/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py index ae3fb46d..5761a9f0 100644 --- a/sed/loader/fel/__init__.py +++ b/sed/loader/fel/__init__.py @@ -1,12 +1,11 @@ -# """sed.loader.fel module easy access APIs -# """ -# from .buffer import BufferFileHandler +"""sed.loader.fel module easy access APIs +""" +from .buffer import BufferFileHandler from .dataframe import DataFrameCreator - -# from .parquet import ParquetHandler +from .parquet import ParquetHandler __all__ = [ - # "BufferFileHandler", + "BufferFileHandler", "DataFrameCreator", - # "ParquetHandler", + "ParquetHandler", ] From 1f954087737358c03e712c51094311a9647c9408 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jan 2024 21:37:55 +0100 Subject: [PATCH 019/300] small fix --- sed/loader/fel/buffer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 3631e991..f6502dd9 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -57,7 +57,6 @@ def __init__( suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode. """ - super().__init__(config_dataframe) self.pq_handler = ParquetHandler( [Path(h5_path).stem for h5_path in h5_paths], folder, From 8f551d0068867e087f7060445a3b455d2b678f08 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jan 2024 21:39:17 +0100 Subject: [PATCH 020/300] small fix --- sed/loader/fel/buffer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index f6502dd9..3a8dc9c9 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -57,6 +57,7 @@ def __init__( suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode. """ + self._config = config_dataframe self.pq_handler = ParquetHandler( [Path(h5_path).stem for h5_path in h5_paths], folder, From c85fdec3918b387221aa67d87a93455863820c72 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jan 2024 21:50:45 +0100 Subject: [PATCH 021/300] small fix --- sed/loader/fel/buffer.py | 19 ++++++++----------- tests/data/loader/flash/config.yaml | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 3a8dc9c9..6c63ae4c 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -95,9 +95,6 @@ def schema_check(self) -> None: get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), ) - if self._config.get("split_sector_id_from_dld_time", False): - config_schema.add(self._config.get("sector_id_column", False)) - for i, schema in enumerate(parquet_schemas): schema_set = set(schema.names) if schema_set != config_schema: @@ -149,14 +146,14 @@ def create_buffer_files(self, debug: bool) -> None: df_creator_instances = [ DataFrameCreator(self._config, h5_path) for h5_path in self.h5_to_create ] - if debug: - dataframes = [df_creator.df for df_creator in df_creator_instances] - else: - dataframes = Parallel(n_jobs=self.num_files, verbose=10)( - delayed(df_creator.df) for df_creator in df_creator_instances - ) - for df, prq in zip(dataframes, self.buffer_to_create): - df.to_parquet(prq) + if debug: + dataframes = [df_creator.df for df_creator in df_creator_instances] + else: + dataframes = Parallel(n_jobs=self.num_files, verbose=10)( + delayed(df_creator.df) for df_creator in df_creator_instances + ) + for df, prq in zip(dataframes, self.buffer_to_create): + df.reset_index().to_parquet(prq) def get_filled_dataframe(self) -> None: """ diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 3a8289f0..1349fa46 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -9,8 +9,8 @@ core: # The paths to the raw and parquet data directories. paths: - data_raw_dir: "tests/data/loader/flash/" - data_parquet_dir: "tests/data/loader/flash/parquet" + data_raw_dir: "/Users/zain/Documents/Work/sed/tutorial/" + data_parquet_dir: "/Users/zain/Documents/Work/sed/tutorial/processed" # These can be replaced by beamtime_id and year to automatically # find the folders on the desy cluster From 0a7e836f9f546599bffb6568ae762f5827694e6b Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jan 2024 00:19:58 +0100 Subject: [PATCH 022/300] fix error with pickling --- sed/loader/fel/buffer.py | 58 +++++++++++++++++++---------- sed/loader/fel/dataframe.py | 33 ++++++++-------- tests/data/loader/flash/config.yaml | 4 +- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 6c63ae4c..4ca6ffe9 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -1,16 +1,18 @@ """ -The BufferFileHandler class extends the DataFrameCreator class and uses the ParquetHandler class to -manage buffer files. It provides methods for initializing paths, checking the schema of Parquet -files, determining the list of files to read, serializing and parallelizing the creation of buffer -files, and reading all Parquet files into one Dask DataFrame. +The BufferFileHandler uses the DataFrameCreator class and uses the ParquetHandler class to +manage buffer files. It provides methods for initializing paths, checking the schema, +determining the list of files to read, serializing and parallelizing the creation, and reading +all files into one Dask DataFrame. -Typical usage example: +After initialization, the electron and timed dataframes can be accessed as: - buffer_handler = BufferFileHandler(config_dataframe, h5_paths, folder) + buffer_handler = BufferFileHandler(cfg_df, h5_paths, folder) -Combined DataFrames for electrons and pulses are then available as: buffer_handler.electron_dataframe buffer_handler.pulse_dataframe + +Force_recreate flag forces recreation of buffer files. Useful when the schema has changed. +Debug mode serializes the creation of buffer files. """ from __future__ import annotations @@ -18,6 +20,7 @@ from pathlib import Path import dask.dataframe as ddf +import h5py import pyarrow.parquet as pq from joblib import delayed from joblib import Parallel @@ -37,10 +40,10 @@ class BufferFileHandler: def __init__( self, - config_dataframe: dict, + cfg_df: dict, h5_paths: list[Path], folder: Path, - force_recreate: bool, + force_recreate: bool = False, prefix: str = "", suffix: str = "", debug: bool = False, @@ -49,7 +52,7 @@ def __init__( Initializes the BufferFileHandler. Args: - config_dataframe (dict): The configuration dictionary with only the dataframe key. + cfg_df (dict): The configuration dictionary with only the dataframe key. h5_paths (List[Path]): List of paths to H5 files. folder (Path): Path to the folder for buffer files. force_recreate (bool): Flag to force recreation of buffer files. @@ -57,7 +60,7 @@ def __init__( suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode. """ - self._config = config_dataframe + self._config = cfg_df self.pq_handler = ParquetHandler( [Path(h5_path).stem for h5_path in h5_paths], folder, @@ -134,6 +137,25 @@ def get_files_to_read(self, h5_paths: list[Path], force_recreate: bool) -> None: print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") + def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: + """ + Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. + """ + # Open the h5 file in read mode + h5_file = h5py.File(h5_path, "r") + + # Create a DataFrameCreator instance with the configuration and the h5 file + dfc = DataFrameCreator(self._config, h5_file) + + # Get the DataFrame from the DataFrameCreator instance + df = dfc.df + + # Close the h5 file + h5_file.close() + + # Reset the index of the DataFrame and save it as a parquet file + df.reset_index().to_parquet(parquet_path) + def create_buffer_files(self, debug: bool) -> None: """ Creates the buffer files. @@ -141,19 +163,15 @@ def create_buffer_files(self, debug: bool) -> None: Args: debug (bool): Flag to enable debug mode, which serializes the creation. """ - dataframes: list[ddf.DataFrame] = [] if self.num_files > 0: - df_creator_instances = [ - DataFrameCreator(self._config, h5_path) for h5_path in self.h5_to_create - ] if debug: - dataframes = [df_creator.df for df_creator in df_creator_instances] + for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create): + self._create_buffer_file(h5_path, parquet_path) else: - dataframes = Parallel(n_jobs=self.num_files, verbose=10)( - delayed(df_creator.df) for df_creator in df_creator_instances + Parallel(n_jobs=self.num_files, verbose=10)( + delayed(self._create_buffer_file)(h5_path, parquet_path) + for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create) ) - for df, prq in zip(dataframes, self.buffer_to_create): - df.reset_index().to_parquet(prq) def get_filled_dataframe(self) -> None: """ diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index a414a13e..1fd9749c 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -1,23 +1,20 @@ """ -This module contains the DataFrameCreator class which provides functionality for creating pandas -DataFrames from HDF5 files with multiple channels. - -The DataFrameCreator class provides methods for getting channels associated with specified formats, -checking if 'group_name' and converting to 'index_key' and 'dataset_key' if so, returning -a h5 dataset for a given channel name, computing the index for the 'per_electron' data, -returning a pandas DataFrame for a given channel name of type [per electron], [per pulse], -and [per train], validating if the index and dataset keys for all channels in config exist -in the h5 file, and creating pandas DataFrames for the given file. - +This module provides functionality for creating pandas DataFrames from HDF5 files with multiple +channels, found using get_channels method. + +The DataFrameCreator class requires a configuration dictionary with only the dataframe key and +an open h5 file. It validates if provided [index and dataset keys] or [group_name key] has +groups existing in the h5 file. +Three formats of channels are supported: [per electron], [per pulse], and [per train]. +These can be accessed using the df_electron, df_pulse, and df_train properties respectively. +The combined DataFrame can be accessed using the df property. Typical usage example: - df_creator = DataFrameCreator(config_dataframe) - dataframe = df_creator.create_dataframe_per_file(file_path) + df_creator = DataFrameCreator(cfg_df, h5_file) + dataframe = df_creator.df """ from __future__ import annotations -from pathlib import Path - import h5py import numpy as np from pandas import concat @@ -34,17 +31,17 @@ class DataFrameCreator: Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ - def __init__(self, config_dataframe: dict, h5_path: Path) -> None: + def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: """ Initializes the DataFrameCreator class. Args: - config_dataframe (dict): The configuration dictionary with only the dataframe key. + cfg_df (dict): The configuration dictionary with only the dataframe key. """ - self.h5_file: h5py.File = h5py.File(h5_path, "r") + self.h5_file: h5py.File = h5_file self.failed_files_error: list[str] = [] self.multi_index = get_channels(index=True) - self._config = config_dataframe + self._config = cfg_df def get_index_dataset_key(self, channel: str) -> tuple[str, str]: """ diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 1349fa46..c90dea33 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -9,8 +9,8 @@ core: # The paths to the raw and parquet data directories. paths: - data_raw_dir: "/Users/zain/Documents/Work/sed/tutorial/" - data_parquet_dir: "/Users/zain/Documents/Work/sed/tutorial/processed" + data_raw_dir: "/Users/zain/Documents/Work/run_44797_data/raw" + data_parquet_dir: "/Users/zain/Documents/Work/run_44797_data/processed" # These can be replaced by beamtime_id and year to automatically # find the folders on the desy cluster From 4a787eb718cc03bdfe87ce4a317846b57c786570 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jan 2024 00:21:00 +0100 Subject: [PATCH 023/300] use old cfg --- tests/data/loader/flash/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index c90dea33..3a8289f0 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -9,8 +9,8 @@ core: # The paths to the raw and parquet data directories. paths: - data_raw_dir: "/Users/zain/Documents/Work/run_44797_data/raw" - data_parquet_dir: "/Users/zain/Documents/Work/run_44797_data/processed" + data_raw_dir: "tests/data/loader/flash/" + data_parquet_dir: "tests/data/loader/flash/parquet" # These can be replaced by beamtime_id and year to automatically # find the folders on the desy cluster From 084f407d4079e68e40d76e602abd587267ccf9dc Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jan 2024 19:37:42 +0100 Subject: [PATCH 024/300] docstrings fixes --- sed/loader/fel/buffer.py | 44 +++++++++++++++++++++++++++---------- sed/loader/fel/dataframe.py | 3 ++- sed/loader/fel/parquet.py | 36 +++++++++++++++++------------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 4ca6ffe9..90b66471 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -61,15 +61,8 @@ def __init__( debug (bool): Flag to enable debug mode. """ self._config = cfg_df - self.pq_handler = ParquetHandler( - [Path(h5_path).stem for h5_path in h5_paths], - folder, - "buffer", - prefix, - suffix, - extension="", - ) - self.buffer_paths = self.pq_handler.parquet_paths + + self.buffer_paths: list[Path] = [] self.h5_to_create: list[Path] = [] self.buffer_to_create: list[Path] = [] @@ -79,7 +72,7 @@ def __init__( if not force_recreate: self.schema_check() - self.get_files_to_read(h5_paths, force_recreate) + self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) self.create_buffer_files(debug) @@ -119,20 +112,43 @@ def schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def get_files_to_read(self, h5_paths: list[Path], force_recreate: bool) -> None: + def get_files_to_read( + self, + h5_paths: list[Path], + folder: Path, + prefix: str, + suffix: str, + force_recreate: bool, + ) -> None: """ - Determines the list of files to read from the H5 files. + Determines the list of files to read and the corresponding buffer files to create. Args: h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. force_recreate (bool): Flag to force recreation of buffer files. """ + # Getting the paths of the buffer files, with subfolder as buffer and no extension + pq_handler = ParquetHandler( + [Path(h5_path).stem for h5_path in h5_paths], + folder, + "buffer", + prefix, + suffix, + extension="", + ) + self.buffer_paths = pq_handler.parquet_paths + # read only the files that do not exist or if force_recreate is True files_to_read = [ force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths ] + # Get the list of H5 files to read and the corresponding buffer files to create self.h5_to_create = list(compress(h5_paths, files_to_read)) self.buffer_to_create = list(compress(self.buffer_paths, files_to_read)) + self.num_files = len(self.h5_to_create) print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") @@ -140,6 +156,10 @@ def get_files_to_read(self, h5_paths: list[Path], force_recreate: bool) -> None: def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: """ Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. + + Args: + h5_path (Path): Path to the H5 file. + parquet_path (Path): Path to the buffer file. """ # Open the h5 file in read mode h5_file = h5py.File(h5_path, "r") diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 1fd9749c..1be9431f 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -37,6 +37,7 @@ def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: Args: cfg_df (dict): The configuration dictionary with only the dataframe key. + h5_file (h5py.File): The open h5 file. """ self.h5_file: h5py.File = h5_file self.failed_files_error: list[str] = [] @@ -85,7 +86,7 @@ def get_dataset_array( Args: channel (str): The name of the channel. - slice (bool): If True, applies slicing on the dataset. + slice_ (bool): If True, applies slicing on the dataset. Returns: tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index c63e53be..4cefa945 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -1,7 +1,7 @@ """ The ParquetHandler class allows for saving and reading Dask DataFrames to/from Parquet files. -It also provides methods for initializing paths, deleting Parquet files, and reading Parquet -files into a Dask DataFrame. +It also provides methods for initializing paths, saving Parquet files and also reading them +into a Dask DataFrame. Typical usage example: @@ -17,17 +17,7 @@ class ParquetHandler: - """ - A handler for saving and reading Dask DataFrames to/from Parquet files. - - Args: - parquet_names Union[str, List[str]]: The base name of the Parquet files. - folder (Path): The directory where the Parquet file will be stored. - subfolder (str): Optional subfolder within the main folder. - prefix (str): Optional prefix for the Parquet file name. - suffix (str): Optional suffix for the Parquet file name. - parquet_path (Path): Optional custom path for the Parquet file. - """ + """A class for handling the creation and manipulation of Parquet files.""" def __init__( self, @@ -39,6 +29,17 @@ def __init__( extension: str = "parquet", parquet_paths: Path = None, ): + """ + A handler for saving and reading Dask DataFrames to/from Parquet files. + + Args: + parquet_names Union[str, List[str]]: The base name of the Parquet files. + folder (Path): The directory where the Parquet file will be stored. + subfolder (str): Optional subfolder within the main folder. + prefix (str): Optional prefix for the Parquet file name. + suffix (str): Optional suffix for the Parquet file name. + parquet_path (Path): Optional custom path for the Parquet file. + """ self.parquet_paths: list[Path] = None @@ -50,6 +51,8 @@ def __init__( if folder and not parquet_names: raise ValueError("With folder, please provide parquet_names.") + # If parquet_paths is provided, use it and ignore the other arguments + # Else, initialize the paths if parquet_paths: self.parquet_paths = ( parquet_paths if isinstance(parquet_paths, list) else [parquet_paths] @@ -73,21 +76,24 @@ def _initialize_paths( parquet_dir = folder.joinpath(subfolder) parquet_dir.mkdir(parents=True, exist_ok=True) + if extension: + extension = f".{extension}" # to be backwards compatible self.parquet_paths = [ - parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}.{extension}")) + parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) for name in parquet_names ] def save_parquet( self, dfs: list[ddf.DataFrame], - drop_index=False, + drop_index: bool = False, ) -> None: """ Save the DataFrame to a Parquet file. Args: dfs (DataFrame | ddf.DataFrame): The pandas or Dask Dataframe to be saved. + drop_index (bool): If True, drops the index before saving. """ # Compute the Dask DataFrame, reset the index, and save to Parquet for df, parquet_path in zip(dfs, self.parquet_paths): From 73802fa80afbab91b6562804ccebcdd513eb0eae Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jan 2024 19:44:54 +0100 Subject: [PATCH 025/300] fix tests --- tests/loader/flash/test_dataframe_creator.py | 96 +++----------------- tests/loader/flash/test_utils.py | 74 +++++++++++++++ 2 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 tests/loader/flash/test_utils.py diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 3c569554..c8417d8e 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -12,84 +12,14 @@ package_dir = os.path.dirname(find_spec("sed").origin) config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" -# Define expected channels for each format. -ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] -PULSE_CHANNELS = ["pulserSignAdc", "gmdTunnel"] -TRAIN_CHANNELS = ["timeStamp", "delayStage", "dldAux"] -TRAIN_CHANNELS_EXTENDED = [ - "sampleBias", - "tofVoltage", - "extractorVoltage", - "extractorCurrent", - "cryoTemperature", - "sampleTemperature", - "dldTimeBinSize", - "timeStamp", - "delayStage", -] -INDEX_CHANNELS = ["trainId", "pulseId", "electronId"] - - -def test_get_channels_by_format(config_dataframe): - """ - Test function to verify the 'get_channels' method in FlashLoader class for - retrieving channels based on formats and index inclusion. - """ - # Initialize the FlashLoader instance with the given config_file. - df = DataFrameCreator(config_dataframe) - - # Call get_channels method with different format options. - - # Request channels for 'per_electron' format using a list. - format_electron = df.get_channels(["per_electron"]) - - # Request channels for 'per_pulse' format using a string. - format_pulse = df.get_channels("per_pulse") - - # Request channels for 'per_train' format without expanding the dldAuxChannels. - format_train = df.get_channels("per_train", extend_aux=False) - - # Request channels for 'per_train' format using a list. - format_train_extended = df.get_channels(["per_train"]) - - # Request channels for 'all' formats using a list. - format_all = df.get_channels(["all"]) - - # Request index channels only. - format_index = df.get_channels(index=True) - - # Request 'per_electron' format and include index channels. - format_index_electron = df.get_channels(["per_electron"], index=True) - - # Request 'all' formats and include index channels. - format_all_index = df.get_channels(["all"], index=True) - - # Request 'all' formats and include index channels and extend aux channels - format_all_index_extend_aux = df.get_channels(["all"], index=True, extend_aux=False) - - # Assert that the obtained channels match the expected channels. - assert set(ELECTRON_CHANNELS) == set(format_electron) - assert set(TRAIN_CHANNELS_EXTENDED) == set(format_train_extended) - assert set(TRAIN_CHANNELS) == set(format_train) - assert set(PULSE_CHANNELS) == set(format_pulse) - assert set(ELECTRON_CHANNELS + TRAIN_CHANNELS_EXTENDED + PULSE_CHANNELS) == set(format_all) - assert set(INDEX_CHANNELS) == set(format_index) - assert set(INDEX_CHANNELS + ELECTRON_CHANNELS) == set(format_index_electron) - assert set( - INDEX_CHANNELS + ELECTRON_CHANNELS + TRAIN_CHANNELS_EXTENDED + PULSE_CHANNELS, - ) == set( - format_all_index, - ) - assert set(INDEX_CHANNELS + ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS) == set( - format_all_index_extend_aux, - ) +H5_FILE = h5py.File(os.path.join(package_dir, "../tests/data/loader/flash/", H5_PATH), "r") def test_get_index_dataset_key(config_dataframe): """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" - df = DataFrameCreator(config) + df = DataFrameCreator(config, H5_FILE) index_key, dataset_key = df.get_index_dataset_key(channel) group_name = config["channels"][channel]["group_name"] assert index_key == group_name + "index" @@ -104,7 +34,7 @@ def test_get_index_dataset_key(config_dataframe): def test_get_dataset_array(config_dataframe, h5_file): """Test the creation of a h5py dataset for a given channel.""" - df = DataFrameCreator(config_dataframe) + df = DataFrameCreator(config_dataframe, H5_FILE) df.h5_file = h5_file channel = "dldPosX" @@ -132,7 +62,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): """Test the method when given an empty dataset.""" channel = "gmdTunnel" - df = DataFrameCreator(config_dataframe) + df = DataFrameCreator(config_dataframe, H5_FILE) df.h5_file = h5_file train_id, dset = df.get_dataset_array(channel) @@ -150,7 +80,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): shape=(train_id.shape[0], 0), ) - df = DataFrameCreator(config_dataframe) + df = DataFrameCreator(config_dataframe, H5_FILE) df.h5_file = h5_file_copy train_id, dset_empty = df.get_dataset_array(channel) @@ -194,7 +124,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # """ # Test the creation of a pandas DataFrame for a channel of type [per electron]. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # df.h5_file = h5_file # result_df = df.df_electron @@ -218,7 +148,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # """ # Test the creation of a pandas DataFrame for a channel of type [per pulse]. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # train_id, np_array = df.create_numpy_array_per_channel(h5_file, "pulserSignAdc") # result_df = df.create_dataframe_per_pulse(np_array, train_id, "pulserSignAdc") @@ -248,7 +178,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # """ # Test the creation of a pandas DataFrame for a channel of type [per train]. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # channel = "delayStage" # train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) @@ -270,7 +200,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # """ # Test the creation of a pandas Series or DataFrame for a channel from a given file. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # df.index_per_electron = multiindex_electron # result = df.create_dataframe_per_channel(h5_file, channel) @@ -286,7 +216,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # config = config_dataframe # config["channels"]["dldPosX"]["format"] = "foo" -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # with pytest.raises(ValueError): # df.create_dataframe_per_channel(h5_file, "dldPosX") @@ -297,7 +227,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # Test the concatenation of channels from an h5py.File into a pandas DataFrame. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # # Take channels for different formats as they have differing lengths # # (train_ids can also differ) # print(df.get_channels("all", extend_aux=False)) @@ -328,7 +258,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # config = config_dataframe # config["channels"][channel]["group_name"] = "foo" # index_key = "foo" + "index" -# df = DataFrameCreator(config) +# df = DataFrameCreator(config, H5_FILE) # with pytest.raises(ValueError) as e: # df.concatenate_channels(h5_file) @@ -341,7 +271,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): # """ # Test the creation of pandas DataFrames for a given file. # """ -# df = DataFrameCreator(config_dataframe) +# df = DataFrameCreator(config_dataframe, H5_FILE) # result_df = df.create_dataframe_per_file(Path(h5_file.filename)) # # Check that the result_df is a DataFrame and has the correct shape diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py new file mode 100644 index 00000000..3773756a --- /dev/null +++ b/tests/loader/flash/test_utils.py @@ -0,0 +1,74 @@ +"""Tests for utils functionality""" +from sed.loader.fel.utils import get_channels + +# Define expected channels for each format. +ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] +PULSE_CHANNELS = ["pulserSignAdc", "gmdTunnel"] +TRAIN_CHANNELS = ["timeStamp", "delayStage", "dldAux"] +TRAIN_CHANNELS_EXTENDED = [ + "sampleBias", + "tofVoltage", + "extractorVoltage", + "extractorCurrent", + "cryoTemperature", + "sampleTemperature", + "dldTimeBinSize", + "timeStamp", + "delayStage", +] +INDEX_CHANNELS = ["trainId", "pulseId", "electronId"] + + +def test_get_channels_by_format(config_dataframe): + """ + Test function to verify the 'get_channels' method in FlashLoader class for + retrieving channels based on formats and index inclusion. + """ + # Initialize the FlashLoader instance with the given config_file. + ch_dict = config_dataframe["channels"] + + # Call get_channels method with different format options. + + # Request channels for 'per_electron' format using a list. + format_electron = get_channels(ch_dict, ["per_electron"]) + + # Request channels for 'per_pulse' format using a string. + format_pulse = get_channels(ch_dict, "per_pulse") + + # Request channels for 'per_train' format without expanding the dldAuxChannels. + format_train = get_channels(ch_dict, "per_train", extend_aux=False) + + # Request channels for 'per_train' format using a list, and expand the dldAuxChannels. + format_train_extended = get_channels(ch_dict, ["per_train"], extend_aux=True) + + # Request channels for 'all' formats using a list. + format_all = get_channels(ch_dict, ["all"]) + + # Request index channels only. No need for channel_dict. + format_index = get_channels(index=True) + + # Request 'per_electron' format and include index channels. + format_index_electron = get_channels(ch_dict, ["per_electron"], index=True) + + # Request 'all' formats and include index channels. + format_all_index = get_channels(ch_dict, ["all"], index=True) + + # Request 'all' formats and include index channels and extend aux channels + format_all_index_extend_aux = get_channels(ch_dict, ["all"], index=True, extend_aux=True) + + # Assert that the obtained channels match the expected channels. + assert set(ELECTRON_CHANNELS) == set(format_electron) + assert set(TRAIN_CHANNELS_EXTENDED) == set(format_train_extended) + assert set(TRAIN_CHANNELS) == set(format_train) + assert set(PULSE_CHANNELS) == set(format_pulse) + assert set(ELECTRON_CHANNELS + TRAIN_CHANNELS + PULSE_CHANNELS) == set(format_all) + assert set(INDEX_CHANNELS) == set(format_index) + assert set(INDEX_CHANNELS + ELECTRON_CHANNELS) == set(format_index_electron) + assert set(INDEX_CHANNELS + ELECTRON_CHANNELS + TRAIN_CHANNELS + PULSE_CHANNELS) == set( + format_all_index, + ) + assert set( + INDEX_CHANNELS + ELECTRON_CHANNELS + PULSE_CHANNELS + TRAIN_CHANNELS_EXTENDED, + ) == set( + format_all_index_extend_aux, + ) From 70a3c5bfd176333466836abcefdeab5b2a6cbb0b Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 12:40:32 +0100 Subject: [PATCH 026/300] fix certain problems with df_electron and add comphrehensive tests for DataFrameCreator --- sed/loader/fel/buffer.py | 10 +- sed/loader/fel/dataframe.py | 57 ++- sed/loader/fel/utils.py | 7 +- ...ream_2_run43878_file1_20230130T153807.1.h5 | Bin 2250600 -> 478032 bytes ...ream_2_run43879_file1_20230130T153807.1.h5 | Bin 0 -> 478032 bytes tests/data/loader/flash/config.yaml | 3 + tests/loader/flash/test_dataframe_creator.py | 377 +++++++++--------- 7 files changed, 231 insertions(+), 223 deletions(-) create mode 100644 tests/data/loader/flash/FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5 diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 90b66471..68e847b7 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -220,12 +220,18 @@ def get_filled_dataframe(self) -> None: tof_column = self._config.get("tof_column", "dldTimeSteps") dataframe_electron = dataframe.dropna(subset=self._config.get(tof_column)) + # Set the dtypes of the channels here as there should be no null values + ch_dtypes = get_channels(self._config["channels"], "all") + dtypes = {channel: self._config["channels"][channel].get( + "dtype") for channel in ch_dtypes if self._config["channels"][channel].get("dtype") is not None} + # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): - self.dataframe_electron = split_dld_time_from_sector_id( + dataframe_electron = split_dld_time_from_sector_id( dataframe_electron, config=self._config, ) else: - self.dataframe_electron = dataframe_electron + dataframe_electron = dataframe_electron + self.dataframe_electron = dataframe_electron.astype(dtypes) self.dataframe_pulse = dataframe[index + channels] diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 1be9431f..4130ba4d 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -120,23 +120,29 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and the indexer. """ + # Get the pulseId and the index_train index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) - index_train_ravel = np.repeat(index_train, dataset_pulse.shape[1]) - pulse_ravel = dataset_pulse.ravel().astype(int) - offset - microbunches = MultiIndex.from_arrays((index_train_ravel, pulse_ravel)).dropna() + # Repeat the index_train by the number of pulses + index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) + # Explode the pulse dataset and subtract by the ubid_offset + pulse_ravel = dataset_pulse.ravel() - offset + # Create a MultiIndex with the index_train and the pulse + microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + # Only sort if necessary - indexer: slice | np.ndarray = slice(None) + indexer = slice(None) if not microbunches.is_monotonic_increasing: microbunches, indexer = microbunches.sort_values(return_indexer=True) + + # Count the number of electrons per microbunch and create an array of electrons electron_counts = microbunches.value_counts(sort=False).values electrons = np.concatenate([np.arange(count) for count in electron_counts]) - return ( - MultiIndex.from_arrays( - (index_train_ravel, pulse_ravel[indexer], electrons), - names=self.multi_index, - ), - indexer, - ) + + # Final index constructed here + index = MultiIndex.from_arrays( + (microbunches.get_level_values(0), microbunches.get_level_values(1).astype(int), electrons), + names=self.multi_index,) + return index, indexer @property def df_electron(self) -> DataFrame: @@ -153,24 +159,31 @@ def df_electron(self) -> DataFrame: # Data logic channels = get_channels(self._config["channels"], "per_electron") slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + + # First checking if dataset keys are the same for all channels dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + # If all dataset keys are the same, we can directly use the ndarray to create frame if all_keys_same: _, dataset = self.get_dataset_array(channels[0]) - sliced_dataset = np.take(dataset, slice_index, axis=1) - raveled_dataset = sliced_dataset.reshape( - (-1, sliced_dataset.shape[1]), - ) - - dataframe = DataFrame(raveled_dataset[indexer], index=index, columns=channels) - - else: # 3x slower - series = {channel: self.get_dataset_array(channel)[1].ravel() for channel in channels} - dataframe = concat(series, axis=1)[indexer] + data_dict = {channel: dataset[:, slice_, :].ravel() + for channel, slice_ in zip(channels, slice_index)} + dataframe = DataFrame(data_dict) + # Otherwise, we need to create a Series for each channel and concatenate them + else: + series = {channel: Series(self.get_dataset_array(channel, slice_=True)[ + 1].ravel()) for channel in channels} + dataframe = concat(series, axis=1) drop_vals = np.arange(-offset, 0) - return dataframe.dropna().drop(index=drop_vals, level="pulseId", errors="ignore") + + # Few things happen here: + # Drop all NaN values like while creating the multiindex + # if necessary, the data is sorted with [indexer] + # MultiIndex is set + # Finally, the offset values are dropped + return dataframe.dropna()[indexer].set_index(index).drop(index=drop_vals, level="pulseId", errors="ignore") @property def df_pulse(self) -> DataFrame: diff --git a/sed/loader/fel/utils.py b/sed/loader/fel/utils.py index 153ba642..a75c3d98 100644 --- a/sed/loader/fel/utils.py +++ b/sed/loader/fel/utils.py @@ -4,6 +4,7 @@ PULSE_ALIAS = MULTI_INDEX[1] DLD_AUX_ALIAS = "dldAux" DLDAUX_CHANNELS = "dldAuxChannels" +FORMATS = ["per_electron", "per_pulse", "per_train"] def get_channels( @@ -33,7 +34,7 @@ def get_channels( if formats == ["all"]: channels = get_channels( channel_dict, - ["per_electron", "per_pulse", "per_train"], + FORMATS, index, extend_aux, ) @@ -48,7 +49,7 @@ def get_channels( if formats: # If 'formats' is a list, check if all elements are valid. for format_ in formats: - if format_ not in ["per_electron", "per_pulse", "per_train", "all"]: + if format_ not in FORMATS + ["all"]: raise ValueError( "Invalid format. Please choose from 'per_electron', 'per_pulse',\ 'per_train', 'all'.", @@ -68,7 +69,7 @@ def get_channels( ) # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. # Otherwise, include 'dldAux'. - if format_ == "per_train" and DLD_AUX_ALIAS in available_channels: + if format_ == FORMATS[2] and DLD_AUX_ALIAS in available_channels: if extend_aux: channels.extend( channel_dict[DLD_AUX_ALIAS][DLDAUX_CHANNELS].keys(), diff --git a/tests/data/loader/flash/FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5 b/tests/data/loader/flash/FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5 index 02a04d9f9508fd7b0e64bf490eeb2740fccbbd0d..1eafbf33a8af9db5bf8ef5ed0262a15febe67614 100644 GIT binary patch literal 478032 zcmeF42fP*4xwjV}C@K-e1~x!?2T?##=fL3{Kon3!5LE0X#I8|e;;6C4s}L(jBPtp! z@nST_8pmjiEeVzwQKML5AePvB`JQL(HRn5?8fnwr})g?FcMmQKT=`|Y;N zA$#n#|3Uj4yvOcD9(m9%-}YMF_(3sAIZg-0vw!~2r;Xn+Eh_i!oyO%q8^23iSJJQW z&x@?4_zGI!)5h;fY5bH;t{Y#o?U??fr^dlfWaYeeY)yS7#b+JA+ipFfwpnNqI;8@N z5tRv3=Cd@NudqcWz8s&O()cdZH68EeI($HvPv&=SI=`W7lZlzi|FQ<3whm0Jd@P0o z()|8s>%c2*Di$d1{AIOAwch`l1)8n{p=N^U|t$OW#WDI+kKD2 zIXP^XS%>aHSkBk1Oz;`Qrt844G;Yg$n~vkpnElr#d9%>6(ih2a&C)llX3Z)QX?lk; zY=Qpe73ceM(Mp}-zb#pjjc?w(dAxCjluwIh=dK;w$m<{8wn7Z&bYCTgJN{tx7#{z( zwPQGOa+erhe`C)WZrN!-4A(w>qZlq;wrLDoZ#^=GKfZ2E3@3M(5X0S$-Y$mq&rXlw z?6JGXP&+&g&%g`tCVUL7kF4LdJ**2oVK9t7^|e2Rt6}}eudnR_pLc(R zb3%QX4_AW67r|^e70v_W&V@Z-2iOsIhZ*qA6z)lQFzf{#lAlC}1L50{gZayeV18vCymks44TppG{20vJI-CtZ zgbUy_n46eGXdYsDXM)#7yI9W2a4dNL#o)c>cL|uj%-_5&Oy{;mY+u=4>t=o@f%dYV zw&4IUooy(?{W-SHhPKyxy{?_gd(FSByVopxUJ7mJWpFwCBputgKLYD%+sw<*w%88G zg4YafzqU7DL(4L~{c4@X`db(6;r-Uda?Ni(SRaq&Ebt!lFwfE!hoyMOl-T}Xz)#^C zxB@N$^E12wu7XRz_Sh%Z)$wcF9K+VfJ~7@gp&c#f5U^~It+#b>tlAFJeD3q@>-K?E zZSVi71^(yv%3-nmW-UJXy|Q#4)KsDyzGpU#Z^8FTleFu!e#7_9HCG609-UhC559kP zt0bBIW-21xv3CmpZ-P(zd%LM_`Op5|?%B0c-}3wE|4r?yrF>B=@M(WKG;Yg$ znvUatlHJ$#@~5o>J=doIA5hhpQ}kPU;jwV{wH98V&gP#ePQqe3x@aIaAzU!J&r z6|&`sgKR_22XVYrZO9>8kMsVVlP>Q~*oX7gIbM@6picp%q?lYUc<+Y_!z8q-4P zM7TcZn-h*8x=%Pb&tAkA9jVZFEKychAc#MMDNj%iaYXR~yD2jXq>P)OH9 zH{!td1bJ~3za%5he1~vO`wQA;`#g{BUWM!Aop+=R>JU6{!?jk>k~r#_jU=v~a4gWq zNF8a3E2u|+g8PbGGCl0Jz5b7!Uw#0A4F>8tIXzrY5{VHlW$3C(B9XZz~ z+6w#($XjroGV+h9Ls}y;>=f-sJRmf05U64y2A zYCFBxbvl2a_z$QrX(F`OHoG=PyRaT**f_R%jmo{*c7)Wopx#+OLOz=^*KG5%jkeYM zS?fxB_<580`STKazXGML39}mFGs>skuJQTv)aP0HQCmaIrw_;EZ#njZ6wL@7BaR=+ zvP|t=OyYQ0g|L*omM2{rGa-HMIlclmuC9O8&w&5v7-(C=T!Wd3oN#vHT*8${R_H`H zB9ReFZDKN^oB*dJE+&+_n{Y4Za)07cLU}Ur452)iSV|~=O}s)VuO$NWN17$RLD)9Y zfl$_ILMP6pOQI{G^i1?79NdH*VKDG1-MkP}KczCwN?=MYXw{1+kQrzPePLO3gN9-+*ukk9A(H5J0o ziG%!x3gJf1<>m_coy0@9w97wlw5HQ>9{{V3R>eSO#&W|Q9!;56!fI=n{SZ^QdT>ce|5dHMSAJhpoYY6jMaG0*`! zwuRQ%t1XNrpE1y$d^*BWUGdYx3E1a&IF7W(!Z7-7Q`m;`CqSo->%#_cD>nHh{OXyy za5r?s&g;XkyVZx^!E)5$UGfdw+XCiNmwC{MdiEf`3yheq*S2Evi6;X{NU(WZ;xRXCNhmXQ8wxSc+^6Ef^R3aV>h z^?Trda18wA{r79`|M0?^w?24x^ka1w-}=$ylSjVQ^S2`pyketG{+XCsfBJ$E)$v*V zE>Rr=)iLnp9s}w#YD17ut>`oA?m7Cux=e;oL)50!{nVCnbUFPV1?aC(9}HU%k6sjY zbo8_wttVGkM2|B69DORNr+K~=p}G#Sn{jP8@j_kI zN&{t!+G9Zbi#nGYWi#T?x&j(wga3f|!fR-5#T2d$CV2x%P1kbgz(8H??Bzl3}Neeu+9MWn`mBBWS9*{kK;|@6BmT zp}xo%F+c4dXhTfDR;trquqS$8R#&-Jv?0_#MEl~HL3>ma%vR(a0(}`EMEI*x<)C%rTF2iMf8OCL|!KbhJjbuQN78kGGQ?0@X9PcQaAwhD~Dj67l= z*~VFwtcSVlwwqU)xY@#x=;_!?s|ag>%j| zMOZ5`>tuOSu3fFT2GQ^2{c39VYYoR4_j6Bd%U0Y!o@4qwXmj%;bPXyzhG6>yzf6E) zdyc_%rLgZHuFsni@7lg9p?OUsY)&|XIMyotNCo|$cdiiq%B;h=YqNQ@AT0#)j-Q#> z&UG&Onq8w<`y$jOeiqQbam`}g(jTU;D$th_?cf+AO+kHJ|MRU0!Ma6X&j7-K#4*OD z)FK=FK-f+DI}Tju{QW~N+ChI*%16C2XPg(*J!f3GUb=3UYqsM)hw2*m1^#<}UWNt^ zo$!Ciz(htUjr}9~NLIraA?qacl{BU+=RFg{2&MF;OypR0Z^SH)<$y#^D90vFBRn&4 zE}@*CxR_Ap!@|TZg!0S8?+71EJVGdsHlg&#EFr!z&v5=clzy4_IF{v@2hu9BBH;FK&ue@@r^QEi6Y{ zTEd`V^|OgelBec)B<_6D3tyH9}?xTiIAAiu6~*~<0d zQW%b{YGEYhPk;w#<0811dfWzUk){I_2t6pb(shYY4hbUg8cs0hP>cP z?)w=$M7@dV(ia{njw@8lj#46@G5iX z71$O#&4e22Q46?216xh|hCizZ;jyd&U$jwLm z8<)Rn^&>@1b~5+TCpmo(wPxBLb!GFRjA%1#XTKI;-xUzkXqOtm4(hb_X=u(l^~~%` z+K@Rnyni*IU(`mUKJ1uEbHd-wYiIi)*S@CF7J<-n#&JQu>WdlJA8MnHfBi8r-u`0z ziE-LI_5*c`{c7KtA2^;xZ8we!%5&@%+S@wYF3aHD`s6!u-kER%;+T8RLF=JzpOdfi z=gat=AnMrdsJmKr(n{2l?XL#i7Y#Uk#F%m2u$`@-CGBV4IWG&xq2q)x>>SLoSuq&w z3&y^oKE1**%zTJ@ARLFw(&Sm_j`3RS@E0+f(~3J1~AqZ7KRa-lcrn67x^#qDHU33A?M| zM?2XruQRVQ*K6i}7{@v5NQ7%)F?H+CIqit+8{^h_?3~x;g|?v0u^!mKevQ~xZPAyo z4JjV@@`&T9C-K`6CyqbHp?w|4tUey{Gf(xn=>Ngag=rl96QcrO#IxeEWh%uNu z7L(ob&Yb(%U|-S?mL11!3y$r}eL3$#+R@WrgDg8b6NBr!h z9?k*3g9O5iFwPg+<2qbGM%^TSpRujxZ5hV3A`SM*A<$nrHjJN{?Ko$A`8jDGerCnK z)lTNEZ3LSY#(f#TdoIBrlH==;rv8rw#L1GxQbPBQv`wre3P=E2VrHxSBAiCYNeF1Wi1i#RWNlw)~5 z;og;Uzsd(3OH0PMtdwX&*qBu~mo=NP4(E+ok8@c+(Uq{I8^;3^n-G?a;8-?GY(XeB ziBW`O6I&C?ghc6+*^cwZ?7+E9Z^R6aW!J=Rgp$7JHiWZ?lY^UZ1m}<+(}ZI=2lu%& z>~T4Xb13(=M1Rg~;^uq`XAut#ex3_CZbbMA*Bf&M=W=!88bZ0YLQW{*x(YdA zDXulf&vbB4qvR#eb1ZyUm!Qu_lHbSg#}eMG;GWmW+l22`2p@1wKCa+rnS2@7z|NcT zY%w(8t6)+O{1ea%&AKZ*fM&l4o<`e!1GYnp?t|XDJNol9=nw8A_$k+Jfy2<%4~3Cv zz?;G~T)P2oL?^!iuIhr%fjlO{Fw$=U{Ybx*JeR>QDf2eC7j69aa3*?pUv%}(px>q! z?2G^8Xn34@Jp&m!bd8YSy%ei&%zhWR4jucaa2vY!O;D?jjov>7mM6YBd`wv%K~6cx zz(x2?9z}P50G{k!AN~aTRXRf-?wdmWCxCls=D_8f@Z3NA27AIK)NLMIKpp18VA?el zo+ST2!@1ml2JAyU4uHq8!DCRf5q<@D8vDEm50L*N*nn$Y;NkxG65uuLxD1}g789w@ zI^?}BT#QaXctm{|0PU%V`%4}mpGB|{>D>P^mHVc{5bE(u(*GP@A>REjEhu9J*dC_9 zY?uRk_N6aj#ew*T;9T;X3vKZmoQ~a(h1;>$op3q+qwC<(A^2gS<)-+Lpq6rKU}wtR zQyXy4TzHLo{|zpuPS?Sqr2RgujZFu_c;b$vPrnHd!XM$7p^OK3nmX@JSu2pP9bAOX z&xh}9R38q4^?R_V2asAdjW68fCYG z)A7Nb4R!cMhQpzZxx?T{@;n-@r0kpEK-$xu{5pdBT|R)Z#BW9W#=}DVFPFn_x!1bC z2s5~6N2np6@i2$DJ7Fb!D;;4Gab@Ctkh*T7r<_I}L;uU=R)=EHYK*Bp1jt;cLv*(Y*u z$3t&MGx&%9RriT}8Nc^e>-=S$r`0`WT812v@t}vr@8r}+jajnQ66!VHSLTIw5_Qkcm3)HQ z7dl2(LpYLa+Jd$R+8(H%T9$Sw#&EqJ#uL9a=h!0tzLvH|KLqtt%U7qdot7VepNrN~ z*iJOC__s#Vc{0nPEtaj060nE+F``YiD}9ln3*{MXP8#$Wf6MGX6MvU(9o3Gq$z0ox zbMsJ};ky2ssBLKr>Z5HDPTu%+7hA;)rvU#5q&TEdHmaod?Kk2a@!6*BPdLxKGaS7XVlaEG&%j1Id;Z@ z?Zws^L`@Pq6az_Py6GHG<$5MmebhC}K3SPKb=1{?I5o6f z+Zo@1bI8%RqBg1yN`J?A`p!Plwo&(`?NM{1j=6SseAID`c38it>9!*L56<0c-k;NN z5?G^L=Q4C!{UFh|)q;L>J!0(TLVv4$J7*lTt~>fbOCM}pC%Y5p_+Kfuf63vp~R z7XzAt`a)bEC?}3X%5|TK`y!(Mfwe$wHm_Lv`*tL7bQsY@Uf$KYsi z-axDmxXKCb1=NArpEf_ar;!)Qn9?)}9I+#}2xwdKrpNIG$?GvQybAq4WyzPg!e@R|MxTxCX_wp*^8}%G#C*c5*HEIIg2yFI*dv9rdxqx~l{68(-Iv zvM%cHjs^3Mx;fWe^BpJg^NYTT{*3;d+h4S!u%DkHFLkNZpW$_m#d0C;?LcSp^C+VY zd0I2|16a0ee*rni@pD5B>qY8jKe?7^E3r+igZe&fLv9~p?^rI+sA;g#^2D{`8sr>1 zUgGD$mYg@^7y|WjjkHYb5pATcxR#Agbm4kuj_J2z2;rLC=Vu&t(w7$;FH&6tzxX!j zgr8#~=o1n5g1nL__k^s4)-OX6BM2Kane&pV9Lphz!wDr%l)j7eIhP+N<`XvN7S82Y zQ2Hs{^YQogeU$q@%KaX1kWSu8l>Uj8nQPKMu@0fEo9IH=EwK?{$;KSZ@Wcp0(f6=L zqLxs$gh`*mcEn5RUzo|U?4I~0p?oW`AEE4@IDk+NN_?A8j!&FOD5W3a)KMXaa$iU| zgJU@-aRDLZ7bh+ugm7tv-0-prIbpsaaXBG`EB`;a$3Lym@Vu>SIZwWX@C%NGXKzJr zA(UGy=45MjN}p}~is-=GoUuUsp6fn)GHex&qKr?B)VRM)`I>ATv0@Ux@_ZS^>G*t^j+ z?}5G1X5AC89D3&(=&NnO{S=E}J>u7dg5wNT~+B2vQpq%f6`#pXJv&i!ublUf!2YPOIXpctT z5niT_FTrKQ@PEMd)a@7G9trjGhp5LQID`9ETk>8B^f~N8 zI1@f5-A6DMEqD-Z9R@xN>R|W50j|#WVoI2aX;)&y}L91dcZX9-5ngaj^BZdoekl+Zqysv zQJ=}QRsX;Y=FE1mDRmkN-QTJU1L1a#?}izhGIpT427ck!z^YH=Klu4l9sghW@n2n| zs^h=%?hmywbi7QxZ5A5FUTERO73f*<+3wQ`ci>#@L%r8@TM!DGP*7v@n);AB5!W;C z7d17s#;C8Ly+!>EJxvWc>L==7>S1UqQD;IoEX;#EGqoqRE&U2>bB^ZYZ&6KW8EWn7 zk6Cv@(wV+6J$231%cxVVA9)w(P5zcC{?3+rtb2i8lc{G>AAegL^-kM_o)n*Z(uQNm z)H%6d|3%ceEN27aAhBVN<83?8&_o-l*;x;N|Ehiz%dO>n$IAWIHP@aQ z*Ys6v&GA;Gol2P66ZegHe`nIDgHiv0rl^istVKN9S@hfVCB*(}WCcXc6r0ETaV`Ef zcqgu5(*W8p=1n~f%WV*T;n*=oIT>wK7gY08ce9Uk#*ELuihGG{7v-yq`aFX`Ik{=ndcpKr7r>7u z(N8j+?NM^y-AvBaWGJEKJ~DnMS{uX^@ zj=M8fx&iujJ`^?~j`hH?dPS(C*1=NJ#I!8y5<@yfh%eJZu`sKcPW6pTYP z`RHGB96NT&SD!;+9vo+$zeyfYxDI*F+$%s|vpRkTh;Siy4uHM|=bnB?QJatJvFoMl z#paNaj(H*RckSlQIuhrt<+%n^kGLkZB3z#9*v0iPU>DcEfJS6_`Yqx*MB7WvU!Re` zu@B?8wk2%G@nqt#pJh5H%{%6gcAi08E66+U-{QBb`eyRgNsC>?ybFly?Y^9|F66Yy z&zrbkYZAw>tM+ zbe`pI(z%jP-j-uyvdXiw3PQ>7hsXg5{UoK2WQCEeZ{S{$brPKjrDtLUp=3~- zm`Et4-(+`=#b;(o=|4G&V=4V7Cve=D(w}l3@vH!p7()*)g#Tp_IOmy*ZZB7gC0$FXTY3 zHRd4Bk4)r*kSD?koWuXvXK@N~(C{3s(>aH5X5xIp#^5W7{*ntx*O=08vViOQMH-P` zO?(uz}q+xHxBg6}zsnPFDs9z)cq4jM% zn|O{mKC>dpkI}F%;%}r$-vM$y|03l+i1)cis%zj^-UbWO@7UGJpMh=Ab0@%~eejXM z5S~>v3Qc(=_$;KgK)=Dy;96*bPTUrJe$_I#0qyu2@cB(k(VJg@Uz6WmaL+*WYdDT_ zX2Xl{D%?$4HR^)2x1gWDi`M)OEW_{dB1}Wuo&hIP-bv6C{kt{FNKkywNC&M~u>+8bUUi{7;nssOY0nDfDCFthuxPKiu z96!Sl`~m%7KII=n8lTa$3{L9Co&ehYzP=mSi+p#5xAC>S1`iHm&kG!h-R4vFT=)<_ z#|JPQKf@XD9(8*k3T$*E6qI)dys#nr96*1-JoxXv*d30dybCGo$8Z95IuUNi9=E^( z_6=MKr(@4kAW)b0DYFKd1r$USGkl|9K9t|QOOL7z%Je3SGC!NHV$ zJmnrhcog9c^xute2llxemXU|!M_v|fxhJLX#E{%j>e~c9Q_06D^bHnyDxvks^;f@2DO+>%}hP0O$Bvw zHMG11_o=&~M@7_%Am-bha99O>CQ*YPOlVr_l&J}!-KgQH?L{qTYvR$uBGz>%Y4l-? zYw_50uZ4pUl15Lc2qu?IA}aE3~niCUx_9EEzVAT5ea)u|>2AaXt&B7{j#< zIfnR5_;=Cv$T!*-O)jGk)Ci-Et?Oo-Q;(dws;}ywGH>Ieuf?>~A-0cp`i!t*N6xWb z^s8{Jf69DK7yTw^neGK~j72S3jSm|K%E~N@y6M~T_uT5DIpd>XJO$DR&h?!X_J!?s z{Mo__U5p)Hp6E60nvbwRtrHrzjv&^A%WKwSg%vkv<4 z{4IVUFYDW&YZLEtuWVyM-6iUw+C!G-x>VQNFQyF`KY?)+#|`7hwM74bx_0!{b>g~n zWMx9ei2hQ2Z*lEWchOge)}6CXWz0?IXQr+UQQJ12H9=Se_UHK=2V=M+JLn;1J`}mS)U;ieLd96HOjSK z8%A4b59}B}kDT|^DSpP-ALJX?*G&k`)B32FZATsueevsZoa__*8*PbiM|y3e{UPpC z{D5_oIbO8l+8x3hJ0KjUE9_m zTpe6vhg5Jr#6EeI``rIGhU@j*XW5Jk$FGEy2(4S02cOy6*1a(C*=W@@@Qb+y7ReW~ zgs?FIUx<{?&$^pqS(JE`utcB9JFxP|3Z;KUzlfARkr^Dzti%C?ax9#XX#5F{5~~u*YKb)nrE_8+ zp^Qn4B^3Q4GC47oaC#$l_;f>4UvN(ukZ<<$En20e=*l)C*}|e`$DAjedK3y zUUC-45YA4VLpZlW!?UOGljQnJE{2~p;tGx-zp6s{?5WZR(sW-)eiP|{MLy^oiT;t> zIfv3OQocJR*AH@kVi6&f&yk9LkViOY4@kr{4Z;%@%I8Rhr#OdCJWI;BB~9=%T=;yE zxURiO$oKKMzw<3Z*08va;UkIfRASAKYyZcD5Pzp&=z0$Rtf%i_YdF&^kd(Du?;%;_TV#icEKO9A52D1pHJEwd$1=0 z)XKX+8?@$?VNdkvona-keElcKp=Hm8@J3yD?e)5F7RRTMx6fqS2i^N6>hU6+Nj**m z_o+MqEjHs_<&>*$Za^;OCrQ z4R!cX>Y+9HpTqU@U>5!a_hWpMJdTAuc-B&D>bf%cEU#;NV_UdTo*@piYCK zhCFKFMe6itSU}w6u=)V@9)ZujS_{q^$Q~D1MjyWeTVeMpun+7F$Kgjh1$<}Ak6|qL z%z!P)dyH_;w%B=BxQFZa!#$L@2;3Xv^R||C!`B8IQoq6QD7Ib8+I2Pwy#!rEtt~@$i@#y+#W|Wv25Re3uSUPh zjNg*)_gfK+g`&!^-mloiHp|a@?6T zYE*xTWJZkqb0Sf03+q~C;epsjN4MSpuwgRQB9zeji9M(khikH32+?_e3&Guwh|=yPgp znfhOA@NZS()OxGmt-g@ot-e(KQfk9lGsY(KDOc}q!`NcHs`W=7@fgM-YotC)(TD19 z@tq&~dvhqh0p7!H5lJcxcb(g(|+ z?5L$Xe^#y>JGb+8jn18G`WKwnl-Z!cTi#IO(QtC}b6;U4{$SqrX}YsnO@JAbXGYXj?K zxi2BEPn}4=0_j;xf;JG-QYY850s?&%uyOQFvOY!)-!fcBA@+xDp&Zj>peAL%#C;vw z;&pg~^^?BKr4H)JR~v)Rl#)QddLGwL+TdqYz}9}Y>6^%GYlli2`zcFpaxIN}4(Y%6 zS;FU``%wz+mw^5J{PgqH`(l~4)6WZGd_*0tJIA!iz9{^RuzlLkF;x8ySGB>vI|h~{ zUx@obq)7Pg5LpgiLCIAi>BnnMSe{P4e!vni*vcBLh0)W z50D1(hZ2txLiA@mNeI!0QSR~h3+J-5LixT6Ka=9;vHpsGcl=lH|JvJdSr7JGKqoZa z4b*kfv7d&W(SG-YqtS`K2W$34Z|~2$6~On53><_O4By9}a6H642I$=S0-C`jbYtJC zpikj4bnXk`n2z<~+vIg5+=He(mHVcF`!9MyCwPE80*jz8zJ^|)*1kODwSu3bd9Q?z zq&eJx#(X0zPhH<5%|U3<2gBjy@f{dW9fma;5O{HqZVs82Ik$hB9==MXgS17R_K zgGa$#tQD!(N^m9D)c3zlS>J?r zshp}9;2^cZ!#xL*VfPn|vf#u=?CL+_HP zX`{*e`_BS>ObssTW9s(8xf+(g%U2guGta?%jgP-~9ay;*ucK9YU;Ml0ul#+JwDmH~ zsFU~k8(DoC>U;UdoU1#kbNTya^*r?@d?5kUEVJgE>lab4L<5Yz7Ve8$6FOl|Th#EZ zd-OA?f6?aLwsAfFMi%WUL$A~4;l2r65sXvIi@*P-PU?cue_&hH0ny_!(ihk` z`iZQ+I+j}F3_vea=Q2&y=FsOd@{M~DYKfyg8GYh&v+|UGVcYA8D{VwO_cp0`=S=;am#dFyN`kzu4TjxBedS=d;b1B`e&%8&)>?n;#xb>s>#}ypzp$cA35!d z+9~&iO`r{+DXL?dXGZ=(Xh-H4vfYqlyXcqNj!-7U)1%@u4zlSSZ$r9mDM!0t$FiNd z<4W7KCxpy#(6>@Qb?6vx8II-7#M?)t3D(>CG7h6QX&+z%`z@C(NJl@|mW*+uPN|+* zQ1_@qTR-aQ@9higOWoq%6u}sVU262ox^H-aXzk_|7vCMqYSFCTq zbt{f(=M!T)=RWoKIET@t+_R#8QlEENp6igcB4iyXKtGb}r{}R=)(IQx-*A0!KSbOY zK|5S`;~LyIU2DxXZTAlK=XbfTg>f!BZ+TW!{4FlNjA)C}M&n7pHP`nA;t=tdgXHtSzQ$TdHo^i8>T2kl2*j-4F4Tjw|iJZ7Bn!AHJmmI|^OWBOt^hwXTp+QIo>_72chhn*q=}iY{IuWFZnL>RlkfJTOl0B zb;uLp`<%;36@2biB<(|K*mp9Qc==I<@MA)`I5D4479=hwgt*7#3XY-Sy(Cw0UUD_Z za!rNuo#&0boyUHCH`jZS?eTtJ$$p)-3B`ocx% z=kwu3wC}}mF8cuHLRWP3&M+0N-}kp~Mm%H?xu+MJ{V@0~_dNoyQa|@E41u}m@AKg9KI}b!N$CH(LwC~j1NRrS z2j4ky1noNjYOuj|W{Eqy84|@=|C*0W|{}1SY z@V(}@4d8iHuq|mO!fDj`a+r_L;8Iv|1ik{=6exS=ems8)JI#h^_ylHx&&axg^CP%7 z2j82%5gbH4^kY0g+n;~|+`kFbg6{~QL7GFU-vQuzIgTTr93G%Ocf&*M!&w54;VXFx z4#FP0!K_~Fd4RJyJ`2tR-_O#9vV13u@A~kaA6s+JR!~d7Y&e{KE!gWp_$y=TZ5WE* zXJg1{!*D2Q^TGIVj)V`X#|O}su`!OijDk6&IUoK){9|zLAo>qlF+N(tq@makR;Lc@ z!8B~LJ9MV+^rtPuC$kg|r0x4+_oE1RBRqn-_#Tq>?0e}18TW2Udv)yOG2K$cZKA%^1=7#v7;CYT;f+e)^ zAvlS;9|!uT=79TC#=(tTtA4NhH^0|?w$G5NPvo=Nt2#cb_NtD7ui6+u>rjKq)OOK$ zqW0ynx(*slsbl5(6vlDB0~n&gCe8!Vb%}CT$(0)q2sr=n;hBg^BEy~H%Z6**}rkF2niPzD$gF0oZ zL)?3S2IcRh)q7&O=+Ol_P}I1&ma9>fdfiaYLCsw~O07>{iMnXi0o9Sz*r|_tT|gTP zw4*`GS6j0D5nSiK=#N3a3tNGDR5z~C=A8OQEv+x-D|2qU(Vi?K^mr9{mEb4(3<-W~f63d1||RXrhiry|uksvO2JRlQXu=TfJ8O(Pyv(`nVh~ zaeUb3)wmA!X#r7N^S6P$iEjrjiC3$046l{awB=aVAfEB7P1FVTN92sBycgH3lQ!wc zId&|(cM8YY#IcqOV>;@Irh$w$XIpZ8Joj5JWQ_M%cgm=Kcl$s6?)LfG+jUCNHPkZV zdcxY^{BnHgE6bU~8RNp|Xs9Pg{g-i}-_12sqJIIMBioL1Fn#9Oa18DX&g+-C|0#Hq zvKT{=&ZKvab?2OUszx4aIA?yjHU`G7xE44ka@RWMnRC`OnRA=+^drQzWE{tTA%E7Q zsMkA|mxrY)ZqaUPT&2V3q?T>yJ;-asxj_YGc=UQN0NuM#toX4)Yfp*!} z*gnFjnXj78Dbw}QxgUK=gGdiiucuzF!}_fAtxkg1F2WwHZJ@-WWZb&$U^z@_k&sXG}H7c%i z)Vc8UYdCSPQ`94tN&gkCi1%~JHo5*7M}NfqK=gg!JoYnX2XJggKL_iZ_R1hO(^2?yQ27Zaxz$IA-qF+R^#5}^r zEacp0XO;M@ED8D{p!AD$;<%(I$1)Wgq=9l@Nq!^ea#JI2=K5_F@;ix_w3noOXL=0vOCViVnTWBlQe$bmU~pS5`y0k%6%&F zJHT6tGVv@>KXGy8dl2c4M?^wBR0aB?jFd91?gSPyAI0{|)2zWec z@+%U)k9NMSKhLs)rz!V2Sd6~C1l~rUe+_)j)mhZ@Y)T-7%k^Xr36#%L zx&jUR61cK8J_R@n{rP-|`ySf!EHLzR-~Uli)_rh4ZMhNt*avMNUL2054JT99NzfHN z{YA9wdH6l%!izk^>Ls{@;{|Xtc@{7ZeSRSJ7zFjrc`pl;ZLPs>V_+8dO~b~!LtDzc z3w{l&QJ>b}J{k9;yh|Q|_6;Tf2dx`CjUBYbU%S=q-4hv`gVZ>hMQ!-_8s; znR=fI?XlNdFc|x72>mFhJM2i`Oom0|eIJa)r!^j4pdGKl&Di2sa1FM-4h|%*1K==h z`)wG=J-%<|73w&VF)$5IBK~E{cnL;xeRH^pvVQ@-k3>I)&;6?5zFOFo{&%0!OSJ0^ zm_+@jz)xx4RbZXm>+@^!?M0ggLNoIGfV5{4&VfB(Pq>?Omyy?na3bl?gbu^mhXU?z zam@NIlox3GQn(ua8{VRi--JJs=YtUNXG|pYd0MsHV>`y;)7Y{b&y$1d8u+DM1FJrf zubRGFZBKPQs*ZurGzRiIw2aYc8t5iLJx0w09Vn>3p=;@jP``=K${NRUGvXos_I?`2 zXha3*$B54usU<`|k)X!N-xvitm%fdtbE$Pvj%BH<72qBfbw@R?28|uvC2B9~aM0k> zs3#vlY>fI+upE!At2&(elUf$}Mt_23sl)rX%wPGt2CXRSRfgMe4Gk@7>gr}_TJ9ZC zKZ~f9YZvO4ZNl+n!YQP|CIz})#(BKIA90}mRtzIiqyg=!E?1yksb8s;`g>#jA_1*R)IxLaFQ}XK%dvx6o&Au}SJpRJ z=k*dbmFwEtd-NkjU6rzO%8L46cS4RmFDT2l#xX$qq7T7zUYo`Jdtp=AnKk z^EViKJ|hd=w_vR(oTH?PX;&fbH-NF^m~!67vCMrrWTe;E=$wdisSELwfc2&@j(HOE ze~mI~hzI=)`rx8A&i!iW&VBvO<@+icd>YIf{TA7H(v0J}xmk=zE9_5^r^5mIsS`UU=5A_9NWvha&ARkh_ySz9$_k=_qg{YqrJJdYQ}xdf%zA# zyKDFkgr2*0IbT_y;;B%lLF4~th+Wm%^b)sDj?S7H9)g#kx}8xF)k!MSqBEcxTdM6VG$XEccg$I^t<_rd_dj&b`{l-{tyTG5a}C z-@FxRAlLWcng#Z;eeLgi<2!lx<=C{+jcc2c7g#^-o|%{HdJXYgkcM?oUyQcMDATsa z^Q|}r{ckz8@trH`Zm#Ek9_4}in}KC>kDtLAw$I7ayo2#+y)<30U#t^-;d3M7=aBWc zt*O4z@6(xc*V5{Drm77(;n!%)BF-O8tc;IfokU+kY3$$7$1yoEl~D9$%u1C0jPiL} zWq1_V<=8~y_k^5BoLro^j8GON%Dp5DIhQ*VO9-U}b4Zp?v?gp!d(LI`Ms(y@`XmMt z%AkaP4H=WzhETRm%p`2gE}YA*iQNhJNR;25zQy_BO*n${?HB0q;T5TB`aF30CpXz+_%%rz+eBGCtOCFl4$BOFx&~Ii2fl#*{|Y>^N#*_6z8`%KK85pPGCJ@9tFli6eE0O;@Q1$aLx3Eg z$#LMmk0;;oYja@kVDx|dCqHvP0OgN^w(LuogCF2r_;23jaT&aUjdrKa zJHjO5XMnyD{WBMW@1nmE1`NWN0recaKcjqK`6;yXM7W!L?tp#C^P4b>y6*xb$=COr zUxN>3AzV*=ZzR2buB+e}$~Xy5#Yb=q%){4l6F!C8U@C2#1P}DX2LtYnc^q2MXKmqc zl=m83$3BT0VHtTZh5n3(0Z>rR&9F~TYy|Tu=K|P|a`u8LgQz34>%cqSVPo#?2V?2u zG0+ZM_#CaLvHy#(KX%;}R)!9=u@fA_^;6+F`oL$Qt-`hD@F4m95gwra55ofLaS2>B ztm@0Ceg+&}^@&vf7OH+1s*ZuL^%yAB1J$diqn{)lMNK`2sG*>v#NWrOKcOu}@Gs=g z@OMN%G!=+i; zTth2TyNPwB41d3BJ@O%>K~ImmI=Ya)mK@zG)N_oUAKSzA__d_f;kL4Fx}Uq<=1DWmS9)@VC(>rEQ{CRq*VXjmEPa_b8E2y)w*w8*F( zV&l+-c=9Q2Qmn)|1T{T~_C(LhS0O*NGi{~DTF^ELy(;Bs$M{=d+b(Ei1@*AK>aQ7g ziu*9AzirTN1$9=di+bHVXq@&D^^W~w+q!ek-|NxmfsSS$*yciJRj%dgdE&k1iFO+` zM*l8dMxI==&!R4B9coBNzLptvP;G19+17Ok)e4i198VGA3vfKBH>%-jx2UJ88!}ex zGyNdZ7gAlvzSis5zx92V@fe(!%sI!Qb5_5rK1em@To{9nXP=AZ8ljKPaq4l9wxnUc z>cjB&{A$rr=Vp8s`01jaO&R(%;(J_OdzfFj<+9dhmPb1Mm#zo;8gh<yoKh-*vghoT+n$Um-2 zwu3cJ|A;=P7&nm+!UllvM*nhfy`x`jYgw2k0pKv*)D9(rlZO}JI1V^ zVPYQ#sjh)l8~m$dU?%>L*@@-wDM-1`V?4)F`a5QD+?bi1i+&I}A#o<5l>Ut$b6om3 z=5zd5Bc9_}mL^^!l#dcCGWKQVL>oeJPewNwlGvC~Hix>zM8d{Q;auE{Av?j$#IA(G zcc94Lgz`<$ev<04y6WD(xW7;TxV)BlixA4a8V%2$ z`d7z)^}etD+Ibxs@d{g`<`=-3y!ZSx$kCUNfGyFc$G`-%>v7<-wN`^JXy85I zJH+KM5S`im8okk@hr%L!6%WBaXz1H=eOFk9Mm>%)C&2A!&WBh(1ARV7EUbndTEouN=@_*C97b?`GiWxPJlSh99eme% z9r#=={YP8ib9e+l#qVJt41!(o8SM>cVv{*AmwKNM?~`Y9u6Ly^-JlcqcYr{>m&0e# z65{i&*tf6`%%ILY!g=^_egyk)|K6~ed!K`M@I|~1!}{=U4%i!ePlMjrYXIb=IR;Ln z4yVH*#J$e_@4!dot8Zr){7GYb(mptdy7+#S<)Iav(hc7cwB8W=z)XBQ(_mxjG8A^i z#xvk5?Djr(dk;oY#xQu7GA_k-zl6>M@J}!f9)(wF{|7LM_^Gfj_S+w(P`?SFZ^nHl z?&U8{ z3pkE`7Rm~iHJWpDH2o9mZUIXDU4Mzc>-AY4YK#r#bD!Fn+L_O)QAg4@q88@wrVF(E z=wCqVDxj=?hR&3aAZ`}v_Tq1<8qU>|ICigv&n$_@v_T(*T7Ci1KZ8%j--QQlrJnC^ z%)mNC|ApEl8dcQ8Ot&WIEeX->ISnM`0Xjivl}XzQ2_=Z84qmX`l|M-6s&D$CY)} zcM*U0td^E^Fnu4QZz8WF9<4Cz&-u#m4UVld^)2i#p}z`$0|Z^h@x6tr1=&vJ9?-<=TDPqR?DC+dqGhzE7Zya%EE zuDuzHg?)oQ8+|-zzUqhSigBE99(^R-qn24<_l$8G>pGFVCIR)3jC#lRVqeE+>{Ii$ zeL`7|O~<`s%X(>db=Ry5$Dmg2UYTO&%Ki3p(TQu^tN$(Tqp9N>WoML~Ve?`guA_nG z%n^0e(vAh!v`LPA^zE6CzNAQMpZ>Ir`Qdnr8a$!4Qy0}H_FL>b`rGo#aS^q9>q(sp zHC}9%lV==Tj+3E4TitJyWAo@QtS4+uJZ+9zKXne+H)`ZlNH-PM;vD|P?^j=%`=YM$ zCgY|pZEXkqPTyx-Y0s#ccgGeGT46)xbJl|MW*j?qu&s71#BpUmIbR4Jt8osfor`OQ zTF!9NEJqqB7@HaBTh-oiE@^++7aUW};d1?PjdpBi_OoT$Z{RqJ^{`wqf5uY*QA?tH z*T#HR(yBV(6Fm_#Y;=0RNR@2X@N5nbj8pm7+)@j{J=)5g$YyPgE4Rx*M8f$6MoclPAYd33> zWx3AB&zEN?2keVLpTzZ;GO}T$1AQfhc#rcKyJz&LxW~pdTi;Byfi_~k#@`qFJ7bRh z4AA~L_!$%T1+GJ!aIfPeaO_%8Xh-H)>9?{B`#ro*ULRsdudyyT=dD+6A5n*bH7S1f z)m8GT6(Vi$6s2MCl*#9U?L>QSKdC!nrJm z&q7v6tVYD ze0EkDK0%y3lPI5|^)lx^J4@bz_Y$RVrrc|?5@V-LqCKH>fQ|`$HL^|<*5h0{HK8-- z-4lHYrC(wMp=_S0A(YX7htdx;hHEme3EObqm>oEuk=Th)c7fdzdl1UL@U6rFgmQ4= z5JEXDaRedwY_IryujKn_{Qmg3=jC+LL0!Yzqk5;g{3k$$5b-;nQ`{{(k& zE{iIZ@0kw|ah~uQU-3C#i;0uR5>F87OM0?G+!OOWarllRO9}rD@wXTJMk&5~%<%OD z-$CMc9@fFQ7QRQm!sk)kFX`t}GuFN)eAU-Lwfr2~p_Q+tuFmi6UxiE1<*$Hs(YL!o z`COw-(e^WFg|7W^fAnhn0>}6)D)egiEKGnEH%6mJBmWaDCEe3-Ao};=usqinp=Cb+ zTcDEF~-WN_?)Zmv;_F{O5e0@&THteg|7VaR;-SBRA_Ed1)ceKw#$3Fws zAn$eIxPHJrKZLRPWF|t*ChQl14ba^?!9;X+{WJsdH4K7>@Ta&xW;V2=Zd0k(G$`Nq zun%q78=gjgZ!v^@AaE(|nGe1fyg%UpI3FLu95@VJ|J$%9KA1hg_nN!{%Sh|wzucTJq@B8y^hJ!G^QJ=Sz(x%wQL6q>rK=Tg;@)UEn^( z$0AsXa#n;*@p+5{_jB}xN#J`+R;OHjO7kegcguIBe(SpNh!#a=tWPPAu77(_Yx zux@0`+zk(OCx7hF3G$)Xi+h(b9$tZk)KPoX(f)c^lku}2+{d*C;Rf!x8Qx}Iyaf+p zi|@gq3tNwuuEu_?VLk-tPa=tz3CUcCAp++4UL|Q@j z2x=l|83AG$vp83CP&-o-Q(v+U#G7~2snt8wf5~6{QEemYm0VYw6m=o@x2RKRf_|qq z7%11@4+nJ^h`N#OLubp?mC$(9!9r@A+M}AB^;S<(pHxc@=xgdV>ObyBh`I^q+C}@Q zD@VP26OO&lydl#jP|V<%Iu_I+Li!w?DBljSms(>WE<<-xi&DE$D|3&3K^vl9sTrX< z4|b!>{HXLCsDtGzH+>18?{IKBunL$+E~=*hjZOFKxhZazha-L9mjI*kG`Du<2qsJ z!+Gz@J?gF!bwKLx@2}O^v}tT_bx!}oIsFy?-uAh--M|0UQsZ1}$vAY)5yvK!x~pr4 z>z?aMj6+)p5Vcp=sE^PRc4WLdPD{`le6ErDvO2SKINs|T;9O!(<&l=-2piOG^9!K4`6=|4j&Ns`9>nv+e zY%^yyv+)8}HDuA$CBeKBIZ^Ee+({3y=ft<=XbYAbwJBiuSW#G z`bm8=?b4mNZX9D*^`)pWeL%bL1C(RNYqr0%Vf+pCid@H@@iT2Y_q%Sl;vD?U^|L@< zQZAHjUe!5X{Vm|qCI(JDAHcg!2;@5H=#ekn1v!_@5*e5XzOrmn`HMO5Z_zw$`xL z*M0-{CVoeVKOpiDq5L7C=Ko}cu!K19-5m{@|C=0xpS7Q$`aQ7vIq*4-fg}2$$-^S_ z@_XQNwBK96_k#Q!UO`KJ9ri*)-2wXa!jAz<(A^h9pKkaS;2y5+kG`tTx;=X8HgG1- zE1Cnp=i2YQAAd#ZH<>yB9UJC$#IFE9L_huk+z7tM+GknKhNIAgYbjqJ%kdmfq|8Zh z96Iw!@NUb>dnEQm3*H$nLN8wc`;o`D%n!ZyPB;q<{8VU1{cc4kz6*Xuoo)u-LGHWA z58&EQDC1(7M42a}5Bomy9}K}4LpknSX@;h}GIS(weHi*Qnn5i#se#vN%WH5mI{XcA z4z@iT)a`wT`z7f03*cnZ`tFTE&>DZiYT$l}$+Yt#+A$Aa#&$2k?$m!8bVH{f0>4G4 zz8@Y%_g)M=sAGRPW+k4{HG*eiK^=Pf7O*w>?+69ge*-sA?zQlEJN!>Qc)k?Op`Xu% z4#Uv%+w;B;IEHdhg%gPXE)2(ZyU_kwu;!-tMBoYR?DMgFPS$1kEiQmF=#ypoa>%0t z=e1LRbR#-?1xPb1>gPtG`uqq`@-k? z9pH1EOVxUPDZihVK9Ti5e0TJi|Hs~Wz*kXp{eMI6y$cvx=n#6yoehVQ5PImncck~u z(mMo1DJs&7B2A>^Mz|Dd(v;qdh@dElNEiOU-`w3i5am(d$M^mF$mR3-GP|?0Q_h?@ z_vV~CyTulK8n3~`3it9Itgt^zKR5mc`M3WnPhX*Y`siCjJ{|Dsz<;$4&_`v#S7jl# zp<@j2v(v}#A@su$__gRmx9BtC?~O|18hlMkoPP(=hfn`H{8=9OlC+-+zY}G<^v&tm zMS0}&8hmW}cc)!Z9zJ-sp?zz(m)AUOA3i1ex%k^x5ATDYUB@ZVhKF$&_7(nq`uPp? zL;JZnM*8r09pyMC3$YBw1Q>g8{QY__OLbhYhU>US475oj{TketrehDVM*EC`*j^iB_m)T$^*uCZ$VBd z=V5&uqb)DWPQ!L#|Ew@v$G9!_l{&$GK!z6WFYEd(UGnAm>-Z^j$+_iDy zA%0lHB)v`S_{VF=&ta1bDamgWh}1!FOe)w^MoMZvM+A=R*fd1i*rp{?($>a_e+}qyF-Eb*i}RjyYb)}}HZL*HtU??@ zzu)6Cq`)}n#d+_zg?$LbIN`-Ys^MqHWiQqhccl5ALYxTo}uuc^Z8BR!GHe%Jcf__5y*+SLS8VdFyb7D2e8~^_{<~04P3tt zf)Pt72l$?lt6&Ix=!3yp_{-OU=7?RahaZ12D1If(2nWuym-tMu`JgPWZH5p2W5D-| z7X)9TABTY-`gaO(j*B23;vVHuzB=duzkVdfH3NLbGQbb_8=xur)e7*PCLiOuQR@Mp z^)wOm1AO=R8@O*5;su|g-e^$2IQB|_u7QZ#fX)~r-}TPe(SEQr2yr6Nx-`mwe8?*Z z)`G?0!ox7*7&whM$tA#jK7+t*^y>!5k21Wc7Z?K$qWu#fUrt;DH4yLNyJA*?3g{R2 zo6N%a-v@jaS8dP%M?u&{1H-2~b_1R~GeLC=$ z>45%?F8x)7K@j{r^p7E>uZq7{<^BW4F+8v19KJpSK0W%Q=ohDdUFZXYpN#%8`l9GR zvf!)IzQ6J~CQki4Mm)ko8y@%or4H_4-Sn|N7Zc#Kg!wla<1%~(2z_Tle_IZeLwk(J z=(t4yQYWs!YwTY~+}G86toI|(zlV5%_Pb#`o@6+N-_!XWuh7?rID;M^#>lt>eU5^0 za4(4jBZYsA&tTEMIMl~6F*f0Wzs(}75B1WYr=QR99r{n%0Ue)dih6J?ke9)}7XplB zpgqPt4Eh}5Z#8V+rw8NUHOhwmQs zFn>gBU!s0y4dkO^L79+G-)C2xBc>A5wtd|wjC&gZ)M;@3fK>|ROM6r9&(Z#8jD<4L zdP=`2`pR-zXCXKGGGpov!nLyAK5Abt+VDVzY{OF@DaVI#QifWtm2p4i=E6DkAq>Yp z8}rJ@K*w;YUx2=RPZAshgCxZ{)<1Qd*BGDB>l*9Of?iwD8Lm67Rg1Q)9)ON9UdlZK zw5ND6j1@D65o1#kqo9p3XuHsk#WfCF z<-xHNOA+)#_lNy(Y#HqtkGb|yFUQ33G8V#n*>8?l+Y89Z6A#A;0PSHI;2JCk*q8XI zhg8Qo*K{zhGcHeCg7JC)_YLYjcbr4mLw+9Vx!`;~L)l*d);`g8o8v^EC?h>C%p+xB zao$KR9K*h8yU6`GVK@dJ$^vj+3HRy{#(Wsl(!Ue%Lu#O2$Xh=<5A$MSycWjI_Oy)c zwzUnT9mDwf*+uwSrEM<9i#}MikHCTq1od*S2*<4Ti1P-S&~|w^W@6XP@|2sfPRNxp zAqY;itQ=4W;o7=QTN(R~tLP~~$Q8uHI z0&6VN@m?&Z6TMiIaE(m1nSylci#U5l`1~m&(u;+Z7>vJ=w{7Ml1(uDm0O!D3^eVxBD(jpI*kJV=3s{UXFT?8W*5=is-VAr_WN)6h~Zv?@;Hv)T2vd zj|$+v3hu9<@Bb?3g|;3bR`L*JM$E+@%ttI>HW-iC#yG&3#TAhMH9VsTEXP7%i2Egu;@(qW9q#wIP%q%S0)i4*=h{vI2Vb4ww&_@cCUG z(YGd`59+kQL&ji=Krg{`#I#0%eUNPy$R-!?gAC$d)mp~xqq8LGYsc`PIfv?Q>KwnJ6_j&JI?>-%PwRNDQH-z8mf5DTsXfY2dpN`17K=N z*P^db|7I9|8Bb&60s5Zk$JWn9Vypoe^vxGXN?#4?@B|oC65>S1?eATOvl5vezgAvYJxoa#eoMt zBl-a852PPS`>g0UYX<0}WLaAV`uE1TMqiVMWg#z37Mueu2gt&pPYP}Gx%>3-X@5U` zX(<85!2KZxux=rqK|dYmrZiH@mVe)U1u9|;z~hI!9A2zcb{!bA?h|Fg@u3b2{qflM zSmnKW9@cFFjt}EB;721K`p-}&=TFZqVg{ZBATzGpW8rvMPR|4SE%ehOR-ylXM7w%U z3gJ3*gmE+a1qJu$-@s!$7Rpa?iu&<#DlBsgcB4u3auE<8Z%1IwdN2FZ{71V|I&8ugF&!w0Luxw98Q`^USY%rn%7 z@lj7L$eF*l*YPO!8G0npA?gSH-CQRgtS#y#eZSg2i@sCVl#A~>>wos0HO7w|26>Vi zUJU9npfA~D&$D1IEMP$X+D8MwjED1#_2hy6>$wR)N?neRJkBHMi1Uki*7FGc5zIYp zoz?^DA8iA$SWi3~tVOMVybe9kK6d(V7;}XlYv{Yd?{x+D&}uQWuxr_QUmM5fK4J?lN!74 z+OLGZ(id-`KCUGLYeL)dlt^i-34BxBw?aE*LGShT)F_K%UgO;8cu^X(n+eCTvli9} zxduf&W${0zRM$@V63$72G*{&dl-+lwU7txgurgl zj&a;X+o4>@!)FAaE$QKSF;BW)d{*e+6L1_T!_Oz$DGPc;drsZq=O(dmA7j3RcG1}f zrQJDUIM3j1Psi7&Cm6RChHIEV55~g2SwQb6ZGz(*b{^zLS!f-s zjr<54qg}1<=pW}@f2YWU6n!PSADqX0IOkg78Ud_R*P9x3V|;p?7>{sV=%3bw0PlT{ zZ1|btvw{Da4aA~{48f&X_#-|-EIVGpxXEpsc!*axFiygKBaD%ddN$3FS~k;=k|j2L zhSu|}#kmtF*^Fbd3+%T!fs{D!X6HVV-{KPT$yJ*lkP;`hVqiTGKN}~$k__i0Ey(;5 zUPFFtvLTPWZsWvOisSrwO5&W9u_=d?l((sZ^m(e{ye5bM^=uj-C5>#FAa&v}EpbfR z+H^olI@xqaN_v5T&oKzcWU$Q$q-34E1H1u@F`CN&)^S! z3aY{H9sv%+r``vC<$-|j>COqtz^^bSQ&*o!(p2aH$n zxlerGdtxvZWwI0Dpy54H(jjo-p|DOCU~A4ry_ueCAdV=#RF0fbyjf69Iwn?-v6p;KNS|7NO7Y z0D)Y}gW7neRwGaretN#U<2~fP3zp(|3D}Sadui|ts)Xp<1kA-m@V!Eu1w6w(k4NAw z#1*E1iGcBmc7XA#aPS3Wa0Dd7coKu3&~_Zu8-bY0G{hQagDu7Iz6gvjH)syo@I4@1 zQLZ(pf-!}IrKn>yxL2gIaSQB2y9dB_%*`%PA985{Mnhg#(bu!+^A)fhbu9vWA?uys z2ecQ7`X+;T=-*hBod&*wjIM(mXoK-DKOCn5KR_1WfRQLa0W5;Py#orM&Z1xeSPZhD zudf5PHy^ae9CBYx>enDw$jk-!?v*7VKgORAoIwB1gFuYq0mi-*=@L*D?N;-WnR6hr^ML%G}FbF{G+tVMn60AoDs!B>zU^_S1A^Vz`v z$OigiB0hh(Zw>i$;FZ<^$pJqDu;3RV9{MN%Q>!9U_(k{|XaU+6?vHc$~VZk55-&uQbjlXH7KgfXJLi>Fa;97te2|>zx!*Q(&j#(%DGl`IgIy~@$ zX&(>#HdbK({}X>7t9{4xyVOD%`cmM(p-;tvKT7*9iy#l>wI3Y5AO7B2$1$2C5562d z9{Pn)&KZ;b-8B4%{2el5F8W<3XoF*-?~nd7w$HxPze5@6aTG*Z__XK))z9I=SQvlN zZF5W*zlFZhr^Vmn8k9HuO1j_3bNoVCAbbJFcW~zPD)$Ds21F3|3z-ME zqW6ffZTR!(U-VEOoPz+AAz6@zu?YQ#&}-lKt^erz*56lt)Dhac1eohYNb_U9^C6`U z;Mjw;9iyLE145wl7Ie}|j$=Z*M{FNI;};fzzH1Ou)4EEVMjHd$;;DfY{&@Q78EX`z zv^Cm}LN6KT)3yS7sr|^X9R`k#XBEBv|8mI3deD7Ee`)^+ZIwY=#rSG6r1Yb}7SYD) zSOfH(>r=-h5SO4Y+Y=w<0#JtjY251}v^lVy9*j$muNLkBjL+z?Jw(0q#W?FeA@c3` zf{wqSZTiJ&_i39nkgtBO82ve^k(UU7{*3x?KjR7(@^n68MSSPIbq}Eb+D7nmj`e>7D6=l8 z4|Q5V#~9M$_!jQ_5#>2Auv3gd&{wbh^svF)XCjmZpe!}KkNkl9Xs3XV-*65Ml=Z`T z38d5=_MI?}19aSga|rbF&Ja7&b{e{B1mGS5`C0`4QV;DUKc6|5=s(9nIk9dJ^pMbA zQy&drp`2jOjC3}bSL!XtXT(E!f^jjf%Q0#B`o1szeeKwPGB-}_BLeXb;=E6U>2;fQ zh%1mlo03RLb(@|@pJyn}$tarzNLSc6dqtQ=*?5qW9X1D$k|Q>Zhn$MV6`Yf6HqIUs zC%ujQdp1vyK93UvVVc>-A1TRY6Nr>lun9*>YS`39N?O?9dk4QmX*wY#y=(>{B_qKU zo9Rda-v_Q?EC6snhu+h%9OneS{TKKE=Ya42(C`0Ph4YWR7^{&7_#P0=CrBL}pZ;c) zd7dbogP6S@Zkz+>`CRn%Yv|*rum1p2`0-zWetZz~-jE~S<6pg3{0rnCwK;~AoU}QI zlw7pAg!Hl(ezt19LQ1~&qQ8^!d#MdSBlI(V@tLci_Y2#r_ix`u3iLC-e!?+u_WJ2( z+&)Df?6QOYy#me!cA6M|Hvh>w^WFQmwV&_9FFYSCgr9X4(C^iT54$l)1>b*M@D2RY z*FY`!dnZdTH1K>M7_JPmghdu-7NA?Gm(f11A z0sP31!Akg*Goz20zzCEd2k4t#3o>I&TTsRW664-vASuSncdvg3|Mw$sJs*7hfX}^Z z1Ja;BuY+wt7(2L(w(o-R=-)Un19BONx`%*A@caJ+Jct);0es&_PEZK#m4qDggI;K} zCt#c<6s$$v>p&Rf6AC)QPv0FpM%@OkpDT!12bhL-rUHNX$uooL=v!m7*#%U^eGP%2 z|9mg^Wz0>!LWn_uCAhv6@Es`=0NdIC8e+`clVM;i4>3P&p$nbBr}j9%#ywYoANukD z^(8~ub9cjx9{}IA!ut50^~zu{eDtHir)3}`&=Y;(^Sc(JzlV{31kA;l-vtVF76W@A zligrF#<2$ci19uELm~Tp=*IzY3UP`rKv9${4&1ok11>>#QlS0QNaG`&fcvjw40AB9 zxnL*qcL2(h@s9UUhkr2S27&?+LjvuPUmJAC{R2QJ$Zen9*Nqs{Ch!gVz&$p;zlHyi ze+&C!BEHXO-`e)+z^knT{QIp1{2lNUdn&_E5{4AMI_+fGO;`r?>OC6puh6&0`sin;56=U%AB%n{wpAS|{8dJB9Mf+P zACjOio-n|B>7$8@Josqnub~f{{(Vm}@45cXFZ@3A1sVLi2C#2VAGjC6LK~i>D4zf+ z{cEU;aF2opekuCZ=yRkWlD<8b)Be09IPU7L&w?+JK7S(*%H#&=y%^{V;{*bK6=MtZ zm0BE2S>(|NS_n`sxJDlw{hzEqre6HbrG0`fq)l-hekj5HMlO_LpX@QvU#k6+Xiv+F zzBJ~sTn?o46%zaJJNoSyuM(hR7--+1e~|s8uaMU`9~cW`6;3}T0(ta-+ViM=!l+lb zT@+=}KILiEL`vY7BNqQI#owxocj-B$?-jl_{wq1YG{L^=AK=DVEiH zr|3(DE;ByBeH!{1>nTtNeDpf*kPXKutNq%wQ4Z+%0M{Saa}L1uMjL|lN1Gu506u8? zp&2{nGmMD9Cg@mgd0eOdV|{UrX}>t_5bHs^v=?00dcOnOryX$mz_rF$AlDh~kcB=l zF2i-HpQY6R_hBp=+EIdW={JZMwz7ofdkTeN-TbsPiz zyhQlcg>p#>P$%OwPQA1Rdi}GH>2M6R9Bf-*!0uZd*A4UwF#{cA!e@mQfVu(WR~FFm z(bPyGFBn^|9e)kSkQrs?ft;iyK&s;rm`96aFNAZnEsb$c6Xc=K0vqJWP>`o%>WJOw z{d|b;FovdMPrsmUjN60pT1Aio3qCg;XM?U-@sJl6K<>0%9^S`z6Fxt59_57lIX@co zh4B*FJ;qRU%J%Si#l0fz6Xodp+w>p#x9R`=ymB81SpbgM#NH3$#6&V7wvgLKkdknl zMo6Eh9nMLl%{-)xfp~0oBPGsWkYhM@;vVO4%$Np=k2OaU*`z~CGTCH7`aD^2PMjDA z)0{T>k&+@H(54ttQpP3-DKTxrk&>#hh`>2%X44KS>0r|tDe39O8V5T;5ECH=_f`-) zHWIV9A`*GrR{`|9)8`=t24W|gcaf3>Ubt^Uvj{0!>?P*?>4=vY1hEp$TBOh6?3;+$ zC$Rz70ryDgeG-VN7$n+eCsJVT@nY=7IoWS>5GmkZ2m`Sb4Pzl7=6P9P;{2Ew9k)m9 z!(#k{-yv=Ea}sYL-$uuZ87~Ih|EKwjz6bbh>8ou^n<8e=3QWv{{UGoe4+JIh!Osnb z0~v;5z)=x;SvbTe+=+~$4Z|?$~PzT?KQXEu3oi6Yx;PbE)%5fh8_aiv5jvesz?*Vr)h9AM@ zLfDr9#^e5Jpf}3WXPyyx832#_1IBM^gX3uP7#MS=v8hrU7P zMVV9J4%*_r8|R(q(a2i|_#T>Kpi36W1RTfymlI$+=4uem#{lX~EASIyL2)qm6Ogt- zn~gzF^szGJKz&<-c!mdT1wTT!o>0F^LKcAUpnnJBSOls-j@5zBcD>rR>o1yrUrfaJ zUBYKed^+$->wqUSd@uAr#X$<64Sj-op91_(9{7X|_-wR)iax-6@WmuRKK*6zb$c=b z_|%M=$S;m#_-g2H*M4-AVOc?}ggzX`Lp*743?CY=8zxft%`Nzu=!4>(1id!_eX$6A zXl$Q;Hu}Nf(_&u*{x}csv(NRjvh2Rle?~vL@jd=#g1^W^UlPV8@Ug0KH9;71w=HT!T%c{V~cRg{{)D;*!WmeID3B`t^C8 zaS{Hm--9;jr>AY89TeCEgLa`hjsf=u(7$gLLrP#Dw7oeCJ9-Xlx-9ar&J7>|UKl4~ zyo0f7y?3Dy&T$`)Exa3>?G}wfqwGyLVsq!{zx*E2kgHG*J-C|3hlhPcJAChx zejkV*&IxQA-{ry2AdTIou7`62yTG|S0vm0d_=WR)DrZl|16(I@VOK~JoAgLYUQiM^F^FqG6*6^OqQlw5yyuA7U$Fz_fw3E@gR`H)ju9A)4FEnXNk1d$JW^uE1&nWSOc)1v zJ|5ugf4GHwp!c!e#xa4dbM^u1@5qmF{fQUuc_4Il0M+5=E)R0R7hMSOT_SuBNJ*3_1-?jx7yzgU|9d!yg1?-<^Un@n{_B9g{N$hu z=m8ivs0S?6`6hh&E8rvl5X42@iNIyta}|_9UTN??{NeLKhXCxE08`;hp9XS);@~uV z;#a{^@HOhY3X-FLSqaK-Lb@Bi_s>8$s0=Egf0aR6j5igyTms_-ThJ#Dc!GXD1bm-- z1N5mD;5*bAUtzm}AOLxJK@L2}DmMtnwc4Np`dpd)M*WLGThz~eAcetpZ~$}%?ZH}< z{}7x-OyLqpg!cICu;yhDV*z_mW(UZf7wrHK;v`!@1omn)L-`wsP2B}<$e;TlDk4sB z3FE&EzQ=vvCq}#n^uRIOjL3(%1buN&FOU^=R1MS znRefuXS?X^@81iu+eVsbSksIfL_v+sh zQ}&Rbg??!ND$W`IvFLXMg0hqi<4KemWn)1e-2Y-!$8|wU8Srmm{5F~8*d~3Lf`0M2 z`jopPXDuWAwp$n%p%0P1RQ84b$dt&Ve>MWg90&3&=mYojSjh7IvulWf#X`ptBo`P^3o!Ozt@9y_3!PW3&zioLp+?v2b>es zZ#6`@1~|5L#<2UR-@5=gYQJg@l!Jayeg^uWfAyqo0&lKl#Mi{eT?81UC zjeViMX-CPi^lK;2d!OmWQCPjKk`EC>3yyu?h8^wgWcB2*)vP4`RUF zM@Dkr7wypP<;2z$q_cu}ppdb4AEu0I)teDSOJLgg= z;y%chaV=dB?KR{<8%Ff!0>{%7=YY0UfB{?U!9AQiea`YFK^dImn9uw&@Hx%v7Vx0F zzK60x+q6+0=&AOV!e8qA&f3b2>l`cON8O5%KXgzM;2y}G`+2w@l52u<%YM-I^D~FG z)Z#H^3!kfwVL@gBJXMhg49o#P4=8VKJAL0d|AwCfVi=bI_ll4eHi3w1kRTiG3HdE5 zA)h$=Me5<0G_&c1l=QTTL^{i6K2qYuS)6BSZAISm?8P~8_LLmKF>&HD+*d*_*jz$N z?t-T_26UA8*~CFg;@KoaN|HZE3LKL(vB-*ZlHDc;Qc~Ea7*bN)rYusisem-prZ!U2 z(55+3(#8hgkNMp#=Di?YaP05~jvad9_<4Hcobq2-~ea< zpL}z06n_3AAT!El0-<=uP#E|Lae_x+JMQKC(*02%_dP_w$6pOhfuDXPScp9jb3k9z z-5D^>;L3{UJ%MNN&p!fL;onaUT4C?NB*ZXUBOQ;lFX8~*z!JnS<^sOAWC*B@ct|5q z0)Br1#o+H(U^vPQ12eGyWF-1F2Gl`24M8Z%*967To&qV*r<9;4#>97eXGULpAngR+ z#rWrf=_of1G{tzE0LERifg6ZrTnF3G=dGX;VkMzqC)(K$%0dp@lQNL|EsA5m1xSs0 zmt!BqGO*(bo&^S$6+vtVJV8Hx1m`gB^Po*y#8$unjtO+am_2A`3rG*yau3Hb@Bn>& z4CbTlw*a37)d=jy`1gPgxVJsn6NG+&uIO7gu&)5_2MZye)F__{d=GvAF0>m2Zezd4 z4e%!NRsil}nFj`79DG*RAlz?(thmNH!cbQz_z~?s02cb38|T?T9LV7@`mq;twG(7U z-}z3EfH`UfS+xbnFmL%0Cn^c%qRayDQ87F}3@Ef=f_kW<3UH%NK38oG#`ytg4_)Qn zrDGr)>M9MELx)y_uOaWNpcmSso*qHHM*-itQyT2RJ$pcT)E5DKHt=s}1AQ?O-`^&_ zb?MWAS6K(@)d>y7--pJtSpV>MqyCr4KaNgQ+2_9G)sjGXX!!_^xRE{VOm^BSn^!OF zQ=@bDcI^k^q;KbL?V0CiUl2R}vq{~WwJQ0cmPGcAJ@a@EoO<)U=50SbRzLo1JOA|Z zenx!nT-bv1GrTkC=jY8xXA8hd;~J@Sd9L?(`kKG~KR#xHcf^SkN8gypt|zYF_4K-c zq1D8DsQ-_A?)dL>=+&z@F?yBg^%wOxc26k>{%e)v-rZO`a<=Tn zD4EnK`7EwiW-r&TpbXv;3t|a(Lag)uzo)cSTo{ghOpSHKe{*tr^=5N!Z~A+mx3}|p z%Q<#WSK|Bcm65>9K5tLi`r`;9ta|yn;YI2;t_kld|9P!BJns7J&;R!_{cV)TtB(V1 zz5G4>eNfP+zkgo(|C~EsroVG+{c%PL^$o34qYk!RW z<#r&*)?3F8#MWb{AHMWO3B1e>bSms!*At3*)8DfLiAwtHz>AW7wa#MOfqeGxSJ?XF zNF%I9#lRvpDitqYq*#&AniXqR3a!&JqE_81^{UjWS)@*-7`qVY-0(YT?C%3&>#bu4 zV(anmRQJkWewiKUSg#$_wLcJ=fD0Q z2zM_057XFoAl%kl#}35SG&0ut-{aJ)SO1D9H7e`1Z!hbeWB1m&{(ZTZ-TzO`|MPm~#FJi+iBo1wWMssT z%=EvP$CL79^p=RNU;iEPr0dG7H;Kx5)8G61TPk?VId;|-k1u&eBoO=aHqah-K3jhr zF@z56*}gY^yV1R`Hz%S~kG?&+`&a7Rx?}eqefoB8YZr7rFP*>Ro%>^_FZ;f@!ftnO z`9G$=;bFGE$8NxP{z^z7w*ESwpVw{uafA?oUs&^>qo04x-;MwJT(@k!b?T3;$BunX z)zGW2PPriayisFszPFdJ&i^m718XaK<#8m`oBrPCSVUECxkRsy=zJr6P6Dy*fWLk1 zwyi%-(YjUY29*d5^e+||SR;I}fB8zWejgF!6#Ro}?7tf)*m~=%@7Q|$2djPMioMJZ zY_9BG*QcudF+0$xrq2$%a?11EfMeSM=lj3|TYsFQ6~jyGLuZ}-HQxvR^?T$Z|8v&qUu_5G z*m~>KA6t)|e)!TCCGavku(y_XU0<*L$Lzr7dfsx*=fI1t_|E?65{PXFoWH{p*`E*2 z`2V-#NegVfb?iWFJ$CxxOJ9`0pZEL!*?7{xm-zc5U+l==SHeS2B=V(ean ze_!rp_y1G#f2~*f^ici0+t&GVkZyRa%%rS8(-RM>zjYv7iBx}#nb=e zvzL9|#`aJCo*you>iW$krdHQxW~T_G$0myM3P?UHi9h=ik12``#S~ z;i99@|EVAOh^@CyeX;e}>4z_UQ35Zs14aw)y7sr%_wU((1?_xx;6=&4T4%BCfb(~w z*!ts0!@KbP`}XVJy?s|?I_p)O0)H}%9Zx#d+^dI9y|MN9PgeiR6n~i=NYutF&wTd! z{yjVJpo6!(^F8L35uk6NS^_V#1L3y*IAZ$W_8(np{h}R+t;bG3eCdl4`1RlUUQ7NL zKHnybx5bDT8#W?io&P;fWBZS;xA*GR%X;V7J?ZrC%f0OWe`@}(^~(E=-!HEBliKkY zIew74_6PXO^8e@lq=?S;%v|w4kI9jEC+Fb*Bpv0KN~*SSN%b9>q)4)AGH-EriMOY? zEZTXqT#e0DB`{AJX@0$kglEbn%{mvA3K`o=K+-l6Rp~YP-)Sk;nYgX_rT$4Yj*pI4S0tS%1~!AHCArVnIa1wPmpuRDoA*zVKS$|GR zw;IdiK1#MteM3UKza@?Ob&`b(LZtBQDl#+aWLdp=rrgX@OZ?}JlLX-{r2E6JQocYaYl?#U?W)oiNlc)OBR9kfDmH2M~bKJ zY#CaifsAadBxRye(yZrb30^-{qVM#U8BKagz@u(*Y8AZ%XZ3tz>`NcCs?jY}vAQqRh%MUe1^7B@?o>lZk_R$i@M4 zWoY%cz@kyef6(KqX4_P$B7p+|^V_ZCTD z_?z(_fp+5=6*7*a)^ESw~(J|88M`c;<5er+Y;%+4~d z&~%wPuesFg-B+SRdP?U9wT?HIdY5WR%{vw3>s_N{ZTDVsFMlnG^X(iNb+CiD_turLgQ0RVhb2Yc zuO@2OXen?ftxT;lR))o`FUGJDa{Zm$VkBKE8{h0BH8MAmE;Tzyk`@Ex(Bg@5Y1u?M z`B4=~^y5(ZBJLEK-(tE{wmQmpHwMa-76WBh_C+$LbsxPxT7BAB2Bse<5k`Hf{#kt) zdAPac`!+%(Xq0&FRhIlMhDiL+M#??R&Fb21Bw*M?iR`sP-t9j?MzkF+Q|h*q-3#)| znf*28&gCkybVxZ_l(wb}neCF^!w1NnC6(me_Elv5_?&X5eH}@%xQayNnl9s8=aDq0 zYDud6P36dvT9W6T(K7a2Eh!jMP1bksDAto&GVOej^e#PK-hQu}oO-jHG<&U|96eWF zs=il722QOlg#z12Sogxxs%c4?bg8j~9PTXp`!pA;Q;4*i8YXWC_K{TU8%ewpS!GP$ zT2j4j4_Tjjh$N}hS^`VAk}Rd$$%UrV<<8P3l09c*xixEqT+^-Fm z9`6p3`EO2@;U#LxhOw<>O_LGg>C#(L3@jx}cJ+~zOB;&1g!NylrbHcSC+SXhmg~dX zN!L&3$d~cPO3&^yWz=^Kq~H1}Qot`lLUN9j)OlM<@M~cbnRX!7XNW9K&`0j>Ya&TA zx0YsI-;x%OW=PWvLnJ(3PsutxpDY^^Ea`{Tl4!G~T;3ThA!8d$sx#H(X_avKJWfw( zGRKkwmph4(J)d-G(?-@0A1uZ1w~%Sudq~YvBc)sZGSV<%a|wDkO#1FED-%|ikjSxR zdJu!9pz+rZ8_62OoEzMm3N~$ zNzQb+rKG2`bWc!LK3i5%%5G^QEz0$ix$ApM_l4tRdb8d#zjvt2_~Z@gP%~5-jm$11 zzo;b_!rRK)Bu%7TH%qQ8v?NRTOvzMntn3UOBd6B2k>%T)NxBntCGg4^Ie)jU3`|g7 zc06k$JukQ^QH}9>HC)&SP&!|(2wz?9ILV?)xkACP7ZZ(Miy6bv^lDY zC%A8s;lYodUULtcdBa`nNZjay=>O>He7#+lX3XQg?!kBCD7L@x z^%?G=m0G&qoKR0CFF(nhZ~PbRf7un~PrEKxj0_(9MmzV_pU1k6)Vg1`P?`#^3m0<- zb9|F?&enBYsTdix^F~>xb?o8?U>jJf7j+7?vTrc&!z#Nma7C(f_Echb-6Y*PgjCFYc+wlDXTp z8I3WNjmCWFc8lyP<6{376nx;GFngJM% zqRw5+?H-7_IftW4xZDp`Cy73}ucCVr#&D%#D%Z(~T zy0~Nubpf~*7JT43cX3HD>mPzS|FZq5s0mvO2Jf$RKMHjRv#xl1K6R(q{DwlmT$JbV z!-v@)*Xo8*l#6TXu_;jnP_9Aq-R>`^GpgfY%=Q75U(APV|gwqV)H(T6|265OoMa_Gg9D6X-# z6DPRWT`8cxY~RQ|x`a#L3!QgO=(&)3j4|bgyeqnoVm#2PE!68nZ4)T2G1TX7ndd$C zs-?T!Ja5(~Z#3FRUst%WmfhzP6rf(X&S9>~Oi2-)@A%oWOU{4q4n}_mZutRvd@<@+ z#zXEDS;lgl?$Dye)!47sx+x#(9`weY`}o-{m~(gTs&7R1ztl(T4)wWMtrbx4l=4~(Jb6{Pn=*c-Vx+#a;Rljp@!g?J8Jvdu@-+BCo|}-_&fp^%4{d>N22YqB;6^>JwTai#R={qJ z4Wbl3z4p3iUj*rwDK${p$TN+Why`rrd z16@9z@laF~v`d+C-JouFZrB&jSJfoX+=rE4v~m3GCgexe{qndQcENSX@B83-A0BZ- zCxWR9a{O!=j$t^~>cM<>(9hX!>c*0C2^DN*5Nv~MAm$2sw*~7j!9P9{q-SBuP=O- z=rv7yRwCbGZCmPK>|EEQOSs*M&u5SBi}lXW3fL=m$}D4Dr?3v1L*J<*z2oPN9*cH3 zCfIkR1A_5+;=($J`s&TD>S)MO_sXAYVr@i5MSb8GJpua0HHrSZXruG4Zs4ZA!!X;@waU*qf7oHlhx&ng zqM5%JdO*7i{f|0vdoSiaGK%Xhcdl!}u&2Ri>XpRWZsg`V;MjW%F3rzhEmO`9^@BQE z>P#lr$dRA9XfN*%Xav=i?O)Iz)oY1T(FCn7eAF^Njft!Lt3+VpYAI7voF*y zSB|P1Pp&BUovrGZQ+!AXGN=gd7mo(95Yn1F<+_~ z)7&a$hqbEP<{PSW?+LCP<#wt;#Wt$-5gxU;+zypvLkja$;oU0xg0m{P(@C|qVzkQb zKTx%svr^Sxe@qo#x=@8hZBQdxA5c&DvZzLn)~M{WCaQYx#xVm+ zZBn`4dZOG@uBgc)C#dl&ZmNR)o+{VD6Kdt*z3Sq3dsT3zt*Tk473$o;$*OBVKhwYA zBbB7s3e__#p?Uk%B-MBN3H8&C$Ex3F+ttbWYgI(*uThVXX)th}+sS(@fsI?8f7P7RmTj8%~`?IRsZXA)!CgBRnMY+W{HH4RN|9sRNn7CR(XzRH&=gj zLzT?5M{OGUlNuXzTIKqAiMm(fJGK4WPgF$c9TjKp=PGLaA+=%ZI(2n+wCdCCiYiht zjXA8tQ8hkWKb6tn&s?Hzs;|{zRdJ|W$vdmm;O{r7#ASb0aWC9Z*|RQG%V)l?{2`lM zcaEv?i9b=*Utg|H)aKM5k1DFp zkCWARHhz>Y)E2wQTq{wQl7!_05@t=EGYbsWqQpQ`ZK4q|&VPGYhcs0CZT(ic)IcnUg0CRKw3#ve)Z&k#E-Rjo9 zM{4AdnQF(hNcH6DGG{_@&YRs7^t)vQQbGpxw~RVd;umHqW?>ia2|R52@o zIpxb|s{4RvD(=%1=G`P7)zmnoGS}az*458yW;!!Rt@wPpdNwr*eb}#V{d8Mx%3j#K zw|J3q9Z79g*ip!=p5Rk8bMGpZ@Y!W`qLrW7;noN0+And<8|%MP%R|;V&V+dq}@nWs$+c9dU8xX{O}`nd`w#N(g#=7&qofc z(Iq>p-1o4~%Fa{o_Bo(}_Qo?0&iPOk?i$zpw%}&9^20P{n){p8knU4esgoJZYBvt5 zLsJcNe6@XQTH~`Scij2vWc8xz{4dK@y0VGPB@rjppqXctG5Cq!#+!B?yq!IebVqe@;^~?Hf~q1ZPbQZwl;G!#@o+ zKU=q5wJCzN^kI}r>33MgtG!!w{wZ3`&ig<$D*QmDsB=Iadsb47PqtnabghFZ4nC94mC)L$qQ&o79xaRU`e=~it)#_mGEas(ZyVdjoAF3mbepJogyr-If zoyFYq-D#D!{SCG4;d%9Wy(211h7IcM@vqgj!iJe+@n)55>1q{v@S56JAdwlG^`=^U zW}3QjrJ`D&Y>#U6^M19fPBt@r&~ddS`i}Z!Wnr^y#UIp!U8~fJuRl}ypWauVLyy(P zbgNXUEZtPXLmA9(o3BvsHr}e<=q@UA_z`t4WW9QCO&qh)viWMyq?0Ol`y;C1xMk|S z3cFR+otxD4=r2_64`!?QQ7O%%(~hdZw6)c;`sdWOt`}6}l%mtFOMQb4}ISa9MqMZlH3 zsUP>f??2`F#ajcmJ)i$u$4O7-+WLHcxmNegXU}T=+xt>)Srx*2ntC#od;VVMxOdjd zFTMXrRC#?b%SPPX6?5#|`+Qn5@LIHUJ|la+ZG|3w_HWCx?Du&}>b8^Sh-Y4&)gh)I zH7ksax%c3K16zMcmiUFfCy(bH6YF?A`IowEL9>*}bLU^2si$;){-G&exYxP9BV>C_ z8_cT~|D%xI@lI`Zt~=?Wlu zN)d0J;|K11LC2iyUj?NIIWsZa3++zd{?K$y?)*ZYQ~tNoI~yu*dlZ`b50$@q;ZaDL zPdjfbcr(ij!**ocX+3N>0;`Tfxjy2v==*_ceb6L%QWS3mD^VD$RECM zRl_xZxUAD=mZHtKI>&!BwTi!JFV;uKj~Vmt)iHWhoZoEkK*#gHsh7W3U+n9FLqyl_ zDscN(vPdwwu$F7=d;Z9I{k4N*>MOiu)Nks|AE~EAyzgS_ams(Y{C%Cb<#K|U{&fw1 zwAH!JbX)kWU){HFZ}>~~{`q@jAK&bl^bgM$?_KIn7SpfTb^X?P{rdHPRlj>f@;_Av z>%IDxcfKB!tnjP)czwYF|KH5#wZHP;xt{f6={7f;-4ZP{ z15O+;&oBSP%<{o4^PM~2m^G?gGcT-JV2%h{Wp>JD6y0O}v*?5&1RA8^!JmjF6#0#NpTYY@ioH71m^Wab0 z%>|pHbw5r#+oWX_=#Db$?LA@cfBUexHQ640|E-1}>oHSy*@s8zx!`yy-##mLn_G|m zpv!%;-U#8C*oQMQiv z*!F-^E6tOQ=bDqnqw8c}XFgqJUTN@p2<6AV1a14=%#-nRbMfE>x(@0P=aTYa+3ZaY znv^5uaUtGCElc*bN98Z|IM|=uHy7zKa;$SxUo$8EcwUdY$@yrl3!GQ#DD{l6uH$F- zYS~k7Ik(&IrU;=tR{1YAQA7V)Tn~%WtkHAD^|g6-wASNgRjPzUl>5|t{l{JA>S7o4^$f+_Zj$IErfHmEb}+HTZq>!(9Ib)UL%sRdUB5XNT;E755aaM2+?(8EUYK* zzD3#w(6*$#_l3!|MV)58v&LzIs2}gY@x)v`!w3mDlRP9_{h!SRJ-6w$X+t`0sChB| zMw7OHYhZWDM`rVHFKT^d`;@_=V{W}Jshga4t}l3^bsyP(&L4G%b6hvlZ7w;w!7QEn zOFgI49)7Fs7VTTh$rrR-s9&^Ew542UTwAlwU(+_>vqdL$9&HlkN4c;M)FqC4x+iZ) zmZ2-nq-$^KHsckD()P1Rlei(20cE&k)*5r>+H@gYTkIeEMt$b`rK~77_K|CwdDNZ4 z0lUmG$2{iX;y;+w-Ptn^YCYvzt5AKT)@zP^Md?dgt~{pAdVAz&=AQoeyAPiowC}fi z9nrRcI>7Zn*mmI_ADB-Mj>94*9mprxVT^IG3CiMM7dBWIezL4b(i|U zwL@K?p0S^l17*$6&5zu9LTDd3S6rXX=3dt0;u@gc*nZ=jp6?&VX41OFI_{ND7E-M4 z4sB24om#8gpzho|c2&8Q)XdTA}jpFDazvxh(P|o03LFy2!Gx3^@_l zU!FCnB6s&-&v2UR(yFm3#SV>@)kYzyI{24z6-M=zbYsfP=7Qa2Teqar@3n&;XS|+L4v63_@W=hZF+2v;Hjxwz=_FbRLC3ijxle2k8ivQsT64g9|$jaBH*0ACd zQ9MlAw-_fk2UnDfjmpTzIRhj?|6QBt^HZ^7NZ#@@0QhRt-!k?sc`~Qpa}K zdtOtvU9BMZ_SKg%DXL1#tP`a4qdHRLT1}b0eVR<{P){{1yed96aSd4 zQc&(C87oz5wvqkYu~)luUTK!5u6!BMOFBPJBoh+OlY5C*%J6dKB*l;8W%$9tGPqJE zIeQ;_dC|wlC+lK=_b_=QPCMy6qNXehZ7)%s+DV3XQ)J>!kzdZ>Spa#4OXptgWVEU) zHzt&o{=2HloBPPxhze4@W_AgFJEv^UFjo4<=_rXOjuutB zn#>;(DW295GO~M3G3PgyUa8v1>JNHIymcw1U7jg&tz(#UHG?F9e@|(Ze6VD0Gg;=f zZ7sL^){~sIuy=G%bGcWeu{7BnqW7oQX@a@@e4L#7p{^`C)>Ik{?If=7PI9g~=5@$? z@$cJO;(RbpZl!A_TZ@g6iCL@2qd^U1)``Z_BC{oz24K(pnaMKia5ouNt-IW4*+r`5 zttJ(-4VDcNouqEoN|H4F7}=b(q6}_RS?bK1B#)-NFMhMe%Bgwd#d9@C*2z>U*sq^- z@7GzAI_;?9JO=j?4^|0(qLq z)=*1UBy`DzuwJrjYFb%$Y@BotY$GA*dP#vsljPeG)um4QT#}$|21%5#n9P_kR=)qC zn{;?%yllMtq4e$CR6-ukl3C;W$jA``WYfb6(rrOW$(gFO%!`~QA)UraNX9CX`@SWm zL;FhqY7M2{)zRW<*ibSh!E+O4H0zjghB*?Ig7N zJSi}7l(eYZOEQcSse3b}Os&~eay)d&y)(n5b+(#P@5H+@!P8sjjJD+evG*QORc*<> zHzF#EIp>_Sm|(A(n=(ht$AE~KB`Ass0hOTSpol0U1`HsI2}RgzgCK}t07X<3BVs~C z%voR6>Zk3w=k)Eq?~U&p&^xTT=B!!uuUa14U(K>b);MOnKQ=b*f>u3;;W)RJ zh>urd|Kz4vu~>~kQI>eN!6;0*>WXDr+vCcUHn{ALITpU0k2nKy#8rDda>WV@oEK9c zgaz2E)->Gh;Di>>eX+dp4AhhT@#HKQ^msZ3<5TBjxdz1cH}k;gLN~0kjOrWNcE@8F zu5Y>p?aR-``I(Ke?YID}eq$+C?&X8ayW8RJk8U_B-3N~}v&B2w$&_o0@sZjYTaW9F zlNYYQJ_`fTxw<9xBtO=XyinJ)FJ?ZUhhvtmL8}2Rapt){(DD3C+_5;2r{#Fys(tejt`5WKMuylfvng&JO#LyA%*Q1|J+XsVTXZ|N4!gFU zgewlzz;;I+u+K*)tQ$!D``QusxV$Z{KOnGSJvZE_^2CNNcGO>m^js|f(;7^}(a()B zX!K;f7PknS=*`A4{*HK~yfGR6_z#%!rU^`FwWi$uO3;80Xp;016E?# z3~TK5X$FqFJQwdo`r*B~<54@<3myGzaKf&pIPj0I)v6kr3crrSg+F`;8FAR2d!MgcFG0kcfT6>zJkF^mF?6(GwHeZW+{l{bL ztHkNo3Bru^RQpHw!!e7@arYml=w1hKcAt&(oEV138xO`&i34$VhuL_w#Xz*Y9weJ? zdclmbW*YIa#OKPy2{WV245>`)p_IO~qn3C$)yyOQ60?(@Nu6YK;*Qf!_tG{r=m`bH zXfhkv(Q+iQuhGOq`!Q#!>3wqxjki>|Y+|ex=9f5ch1274ejd7-=nI*|9UfLWH@Qj7 zW4OYHG80SuvK$+{Q;uqwD_<=gDyxV!W$uxgO9Qfrcvj-FYiNmuHX@Ewx_7sA0y?^PUd5YmDdmhE8VlM1H?RP@%&&Qo>t+L<3AVx?Ng#20vh%u#Y*Eu z%qp?NnvAy1q2>T{h__n+%ytqVttHkJct6W4tYn6mxcLzIvfngdzW>9>m$cqDiCKQ` zXJA3`rdY}j)F*%#Q)$`ydgR~tDq>kR#H}iPJoPW2S8o+_>J)2Z@`Xfw1vJDP12e+Z zA3lpS|rwjRn_q;noWxtdn-3B^@Q^GVHzHX!yrPt}L!;kYij(*k(U%(GwgG0>h{?yP8$Sz2bP zIUkAfRrc7q=4r{uuPXHqn9h7HX=}B@akC%DUmAymg|ZW|#w`~wQkic(C{a%X)x#_u ziE>XSu2 z<*VMxSc(7UllX1!i=b*wY&g}N1kMNU2f@7+SpT;umne=}W}&-L4+qXat|_cLu47zJ zxhDeaoBA(MkAUt_hH8yB*~Gme+E6VvR_jsEixtMiq;oF>O#tN{*ND22L$ut_forBG zanLL7Eus1sry?d>BAbExF>vjR(0i@umwPL4-wCP_)cc{6e5y|vtfG8kHk#RYjw$Ca z>yEe#hIPccG}XRQ_;6zHm3|bgZ|VmkaW53Ai7Kw+%x3d5G2ivL=4g)7GlhGiux^HGT53s`GW&nV^_o<>Viabc`chCH zu9IFZZcRCFB~eVN7e*GXVsKrccmnsm;5yFQ zQTh*fb^8U?yLhe6kP@kIIqC9MKo+3rfLqSSc}rHy|v& zrigjs2~BctK*it~>Cs&eu(i~OyI-Hdnm<0l`a5bEf2F+WYOxkRzOaRP+F`;byoPA0 zT@1~J-GX)%w?fm6o1mTlXR!W$9?TynL%7Rs*#A5Z++SUSA$tu(Va+6n-JJ{ZBh%n) z+e+eag;{7`zXj?38>rDc18hoH zfcxq4qFwa{qQvSUhz@(Fe}j)3VCZ$q-rWvF658O$n_5mxgvpzHM<=u-bGnC3l( z$OcEDN~Kd^A6Zq{MwS!xtiM23&}C@WBLyxy4TY7T@}OGx-Z01^1HzAt0I!9))Fa>x z7oCJ$=fDkBE1cn+aQZv)1?0GsF2pklw> zV7R~+TJ^XIQ|m;5WzCh4V37!sWs<@6d?m3{r#qArSHb3WK70z>1rEi5VDv(geEsyq z%`%O})2{KL*IHN9y}1kgnrWfg`BccYdIb||RS++x(!5>XLDtockQaO&d|NMsz3N+_ zlX4!`^u7hQx9&sS%s42lUQ;Z-=?9M+UxT&IXW-ts3`omJhSrm|!A$oqqDjp?Xz?X&n!cqVz;B*X>ZTQz~rCyABgC1cH8e3K$ip!9Gegm30q)H`6QV5>Mg{7y#mwf?uPV1XJF{)Jc##}LBD-INZZaqX^Ot6 zWOfC1=Vn6enLFT^aT)5*-vDKf=E3t-F@Uj)pw6lUsL*#m@#9ya!m|(HU41{8+s4AZ zy`IqKSrt)k@Flo;$3V-y^CaA@v=btzhs2)V`OwTj1Gi^ILHKQ5vFK_I(P4iHbkhF>a;tX`HCsnKZ21OO z%o+>BtL%g}-FCv>^jq+$@CxKa=@1VA z=X6Bq+Co@5;0r9dyb)3_=YaXDOK|)CIhYXA1#XYs0ULF!iulwojq8L@X#DA;#MgzuNji_UuQ!SBig*g7f; z`iGPgQ8DMCN;_RKZq{Y!bW0^(j0%Umfh!>^y^83OSY257pM!nWA7kBzYhaVV7@~}; zi%Z$M;=W-Gv43*`c*X1nHP#UWUqpastGh5O{y3D5r2Y%%N5cWvlhCEgUKpQRR*Vmw z2A^8X5NhuS6)x<8p|x@VdQ+VAuS05&C!jNE5&U6XLnO3%0q)(-Lh0m-Ag62v+d;?S z&ZreoJEa)LYZIaIvJ&cDaTTVyl^2x?wUH*7J?8`@Ba4IAP3r7O_q?IK9tURJzxI}M*(oP$FLv%&tc4~SEb!K>$U zIG$VtIicGjWYK;Y=0?wy5#BK4`##v|odr4N_dyGfgK)C)H5k6H2o@By5Pyu}SGF6)T2#9YveO$4h`c`(Kz8#J+XL`0{< zFu>&k#Oyx^bG{WrT;db3Zx#Uh>5pKg>kG&l_6{O0?**S>$HAqefpA~&9OhG>u~v4G zP$peh3?KagE)LPcwzMqRvGW~h%Cryz_C>&cuU(Kex|~o=$b`WuHy~wHB!p=%!0_yr z;*W;u@M+^Kc%}I_HHmecw%7;5rW1w#mci^B+36?ycJ56JQ()pgUM^*Nc zKkfC$_xlb#O#Dym{B;bj*z~7$qoara$z~oq{>{dJIq%(DW&U*kkGbs}$Ny!XAJ_iw z_4K^k+DQlg%4_a;aZ_#TQ_qC&ajjHVdFA(KLM%_I3#$IDT|Sd{t9u1c5_;CHevWUx z;_IJ$#&i7oQ(V5*&#}LL|6kwt9#Q!ef49kLYf|r?u|N0j>!q*6^T+spW$XUD_T#(3 zb&rXoxBlUmVog_No*&o#>#MJqSE6~KoBD&b!C&Ud*zo92YyG&l_knHya*yAdw*AYs zDKF}s>e(~(FW>)|@5gJlVavbRy3AwXU%vY>o?m-a|0jQ5z3TnMIIoL;o72YnGW_t# z)WH5~o0ip0ew}CasSw4!=xCL)Ucb=ze`)V_=P5t+aC`IFB%Y^*wd3E~IsL=cpJRF4 zr3RgU&KsYVlvwG%^s(SE|MlmD-d$ylJos}C{W{LM_911=kMHJ~{*x2k<9WX5=;)vK@MpJA@bj}-g_}R$^R<8XbK=N2Wxb}K zF8<5=T8#EiDnD+R$**(G+$#O)^RL(b>-!_d>y^E99xI*N{W3|tE~U!9+#BD!Z%^-k zIX7uc`(yt5b4}De5_C_c%$ugncd6b|WiL-m?SAg%xCNS+U+D2K=ld}izn;|jXMO0t zOQ?68a!KOvJ-zgQ&c~mB{Pez7!DMfzf7A-@HS**8|C?9am=nsHJjY6h4?o}kI;T^k z5s5!OukBo@j5*M&P#qhz>2K{EJz=}@eR0!1|JKFnk(I%#<^q$Lt@o zc?T|(Dl9qkkz>o$Rx8Zq|7Hlsx`zqobD61TCXP8vX5oJDqRf>tv&cN|wwKSurl>@P zUt~s31fJSJbOXt-N^SeGx%0)s@voV^NvSEg>z)ioH;<|T$vHn zwJ}uRQr!@DyJsrwq4mpe$~?^DRf@<|7)oaKp2$Phohok@3qRoj&oRRn7I{e9Se4Wo8DGIn<;!dlb%|SxQHz%4%kV zdC$y}GndLtAg|3FAn%d=u*Rsfnz>_U9odJ>_Rll!q-G|cnL&f0u?h!#U)4m-TqNJm z@nF95*y3ht=F-{5FB5{4Id}O;O1}W+9ht>s4taukiNd(DUw9ldpu?eyn%88XvHzK! zWj>Slz}&s@*<2;QUt70Pb1#4{Rm-ZGi{v<->|UU-qWt~WTQ`+CkFRd2_KEQo?j!1} znMr5nmiNp2D)Z+&=RdgN0cz$iJ+60C_BK!nA3Tm( zY>v~n72)F6>QqJ7gVJ@>=MPscO9+!ulS zE->%Rd^X3A?`IBwkvc%k++1DFd@WyN{+8Ee`wH6Dd07X{>@pwEJg{9K zV|945ha$q|gu=mc?3tHkt#R(Ku2=^-xsBCq7u(G_96ahD94_;+%+@mJ%=_h7vc@?k zJP*f(`yp`7a@<(2JU7Rc_sx3Y@0ht|o0uDCtua5%dS#z;j?f`}Y~S-&Uld<)ZQ-67tar9AXXJH-UuGNlm-%8|i~Ai+ z_iLl(z80)^UW0pGaIROK@kCjJ=U821yJ-8at1_-)+e1p->t=%$RfCV&C%` z?0cS{^M`YQ`y?ocS5R&mPtw@0HhLU$8&fm%MM*5P#3(`aa#R=z{Az z?}PJ={l@dO=to5qhsv6459ihoJ@7ibhy6Ym6+g3GToc$wT$h<~*Xxq| z^Lg8ww9M=|Ynh*~tXqx+=a17WBjp*we&c=vJO|eujx~?jY*Z=|EZ>SJE<@B!jSK(V zKcoHn-2eUlw+DWE;I{{Ud*HVRetY1z2Y!3tw+DWE;I{{Ud*HVRetY2mYY+V94gX)| z4S(DHTf4tC@LL1FHSk*ltby}YnqonW3i0`P{64}IGw2LghZRe)G;KJ}dn-`&c|3;0 z9DF;)2G!35euy4~I_<5oc=`%Vz3hx@hg;)q>z?@1XD<4?PefHpTXf#M7%L9!h^8)6 z(QRWOzL~xZ{k}Fpb?2$X=DJ|F^IuJ32lPYFw#3@@S%tZ_?)doo0PJq(iaEW4aKx3Z zc&%#)7WEg{Jf{Q36JvhRU^tczor&w|oY*0E7h(Gt57b#1gkyUep>BXP=440W!RwY- zcg8@>T2B9j+IJKNIV?uKWqVPK?T_}g9C3TRDQ=Wk;=J;EFr(2htYGSadSyJYq4jb! zn=u+o{an$W*wOOAfYD3lp!pCpG_!KU>g9XkIQ@yZ;N2*!TwxAA9Jve~@2|wQW8HD< zKw_E`w_#giS=AnqcE$Cl}v@Ob0~ z?7U|k)+@5Z2m@<`8I!Qz!vO5=unOnmc6>K}1GXu11TU@JjSCz$VS&?f9COA5&F=(a z(%P+9+QSA1&I`ws_G_>^@xA>WrsBeoHJC%^n2$N(k9*#&!_t^g)b-wh&m1FhSpN|? z!E+uqZ|H{yvmNoe4V@z&;E9d*%qNa{A}%cFg@dDq;hydb@aSS+JmtF+8y2}@T-*`7 zo#}@L16(mHVFvau-GhY=bS8BR#DRU5;5zE3aDU1g@^u&{M_FO_himb~Lvv#Gop92~ z8F-V<5A9p&6p=b7>{glPHXr2!if7yV|BV6!%;%tnzr8A-@F2qHRC*o9t%~*Zd ze7v{78cXaJ;;YS;m`mL7o<|;-HFzVgupfYns24-8y(%m>as`&maK{$ad@;*?C%zkF zkDux6_f?bTV$mLRycf0%bF6Gp=TakVT5lP;rn%vjf;rf{@*Mm)ZvoQ}XTGdrTTjSKn&nqp4cIE-uSh>t=@rwOCb z6l}55>vdRX^J+}DutcBI4fup=rex@eIZapNTr+EY*CPmnbpz4j^ud*?}X& z{cvK~Dzv@qjm1rtV67UixbxyLg%&*O!dUQ{hhGaEh~I9mV7mSBoE9H>djJ z{Qj4W{JZL?(t-Br%)o{lTFI%CF*$Py@9g40->##s)86JD#hFvXLWO+S;X47D*wWPH2j!1j@T#hRbrweVNy)`n{jD@v*LU!M;zb&Ax_E zULGy^_o@Q!_a|r$rTb}CI6akk-jX6;$tk=*qU-XTT_v(^=tOO3oGf$b=|utpAx=_GHUHGOwihxJ3y zICiD3+~i#cxvA$q$$mqwX z7=qvzm#SIVm-yCOvok4<GH{$|VkOL~$IPpT+|tJ_PY1GYo6=G0UT>0YAu5@}A3 z?p<9)an=;9-6owrjsmUE1BD{-v( zcXijh8oxGYRctHAcVXWZ8XBwG>$Vjre)8d|@8qLT)={2}*Rp=QxP6w6J}D>nI_)Ao zs1q(boSK?I*CqeBRGDI{vZMRS$6A{j8x(!)-?NcoTY++TANfcvw>`gwVst?Ix_p|{ z_I!P*D5EBjpH<<>7BFs*MlKuGSi7gzW@C@Cqg0I)J-3znCtj7KYSokc$oG3*Hj$PU z6>EmOuhveu7N^t+jzPmYUF1to^|d*x?WsQ4%CD;1%iFVCOR+26$fw=Gu=h)e@<6*d z%|q(=yav!5D$-xO*HwnLDWvFurPlx9PoR!(`{?cG9G`+}OcVYfE*ma&c8H+p;L* zwT5%`O}H!Yb*c^WmxpxzHu;}(JcjBY&rkJ7$?w1+$r`SYq$}vx`G(Be;abDFN?Mk7 zWVhCyi_ess*KZ}UPwNioMeldZPx^+)4o*)KiZX&^szq|+cl+fnjSp+;4d}&nQlk2& zimbI+re6)|Nk#jnI<|=NH`$mpB$-kjq`D!M?Kob_JA736o@31Ra_#0CLH<;7jI~C! zODc8pq8fcoraHlOz?jBM9G}~ym#mwoc`e019B-=8GHFAu-MtLeos%-uSFS4fV8F=Kzimn+O?Xme96WH&czP{ldxM7jgg+w{k_tkQ`~n*jpoEi9qon! z$6iPOrZk_l$uZ~W3G0iW8I;c&hsuwXXD8K7dKT5wat#_FHPdoT*q5eMAMG<xaA0F(?S$&Lw7a+ASF66$q`T1VPdx12OuR3|kk!gT^0k zLEf$daLptGqHRM!KmQxddY~^%JgbR-j@gjpoe6&=-Gagnx4^EazG&!n2Atsf9lT521P>}_LgDotFnSi9Z=6|1jOm~wdg>g6vkxADXU)5C-u4=F zJa7z}ZqA2EkHVmj`$q`Uy8?%wT!kwQ9>dA|`=H$PFA!H@7udEbBdXka3-$}+Q8fBhO^GaHI$lXO$J9TWgBg>lMV-fL!R-?FqCq+7DRqBIKm%iOpjl zLQrH7?A#m$*{&C0_cH@=J8uKb@p%hHORvMb>2~m5TSWxadJUx;^Pzsu7l_{+4)Zrh zz_F9*u>Av_>8|eulPA9er<<=}@PZF;vRipE`&cO0-h2T2Tc3me11~`3Nyi|*cL~%i z3WWu0iXoc(GR0a0uPydN__r6Js#HT1(jOfhHl2X7QIFx1X$Dkp^$}89UWKDob7Aaq zU2%K=b$A<=3xiwVgMBrhL;fI7(C>Z_N~MJ8x03ky z&>I{MUWK@BFR7ti9$Xxl0*Xm#QkjCQUfoV!1T2YI6`_4qSYc1_=)i!-~{z(5i~Q2${M8&Q911C%);4?Un99?tvo^IlrGvzV5r|7;^?;kR67T?^`67Bnx{eZ zIt5l)o^beeGMp@a0$VNi!T#F!;WBZ^Iq!?0$MQRg!`@Y`M5Fz0VZh^T_?+qw-rquD+Qk?+V0Z_LM(chDi;Qh^u`8jn#J` z_~Q*YN#|EjKlL3>UEB$MeaZ@xl*>@^M24p;B4D#!N6~KheptND6-+KiL&D}&aQ=*r zcrr5!zLbuE33rb`OmYM9wa-bYeXq9gSa}}OcDX{uPoA(X;0c&Fya1zgQ(yye|NXlb z!<6|iVQbVph#LC|8tqR5+hK>G-22zi!ls-!*XlUTto0BAUhjb(Ps)gS%Wpue0qOT{ z9t_^}30}{A3xUs@i+aOi;lsuq&}YU?C_msbEIRH1wLVrB*@oZXjZ-zzZ?6W%s;)!e z)h|$dB?D?UJ_G#+odb*HVpupk2Syj|f`<>Nmqe9saQe}Fi1gnEWfY-3}H9@4;Q&E3k3(W7wQ>9H!W0fGWiS zMlS9wt~ERk-HZK*)eeCTld1MDX)Mm{m<2l~)D$N16-Cm9gHY>JcTtg;^)@FDgUws2 z--V-LaH@9{k`#+jkInXEjv1dln3yp9e$r1(;_N0ym8Eq4JZ1aJS19Xf$ajn7BTG zfQ+jUnXwkuB@p~KAr{gvY=Z&%d!a|nd(i9l5qeB;hGCjpP_JrPAx+i7;JhUGa_<$y zgrZD(Lp#BZk1rta<2$I8dIDCP41%`{17YY~FPJm(32geF3A!tSAv?9P81i)-G<`4` zJlc4{{$bDHZs#QExIYsf4FkA&Jp+z99)-9&&mqw|1qLsXA)!J8acf~RXzrF1x$izn zgL;0Hj${@ndV(@J47f`6Qf$hT_teXh^eo2_u#*hgQx_ z#9oW4BBp#k%*eO^6&x#zt(MUcJ0J@xY>R{lIxoF`B^i1IoClkNBEU0k{v*~cdtSRg zad=g-hA2GUmep3L?yYlISiHW$lm0ZO&9Iuv82)cW{xR({;De- ztdi8abooy^M-}f;r=~fb^8fTtZuC`Y=}#uoudC+Y{=5C1;T}m(vj57c{<^wej9Rg` zsAlY+?5t_BTiw|!Q@NJg^Pl|hulN4?KIHuIKiz*}V}^3?uVaXvRF538{7>Wlas3}> z-2OWD$NSm^2bJ;Z^=JKCrt!x;|C84l?cV*xPQJgwf{>)krYsf*IOyN*pjZJ*td3OVUqbZSM~F;MnA{=ojrNID*TLfe*LZL+x8=Ve&+FK$N&AA z)_*tGuiy1)=lb8S`(te1Eq~>}e_R{0s_m(itAmw&@pn(xH~i@%GqZos9e*{ir0m5! zX^!HPAM^g7yjmLc`H%MVKb<>hQ)l(zA@_goQ5En&$*1u>^nUL3Kb_Ne;^cp8WB1@m ziH5yyt2e*6udMT*&i!ND!84|R>&q+i8vRK(i5~Vp$4&cE@uyvJ3-Z;sbdM^Zd;jmf zdZ#=7tu3Br$%zPruf9tEJg@5jc;B=+YPn&+d|L@+9_wMkg{rugS zKYv&IFXMMEKKauw@9`%8@!Fe1{@Ul^*R{4C+o$;D*YEzj_eSUczSqWCGmoG4{d#`{ zbv69)xy^}_|M6N;QSH?~?)&xi_Rh|K@eN;}l-Ky@_xlgcQ8b{pdsI@c%0gM+x2x)> z9e17V6#tECSo*hX{JP%%WV1+IQZx1e>czIa#|ED zixvAHv{H`=iWF_`XhhZZ#8vOyDaM>QAwFz6A>`8{zzI9)h}y%+ZyrVGv3IFWCaFZQ+AEAorNgxB+fLVI?< z;Q5*#*NXKU_KM0G8KT;nL&C-4otSkiP>fp?DU4g55>E5O#Wu$q;`z5D!s6yL!TU|J zq4UWXG*z=Lytez3JEDzgy4djTnMgjT6-$=g5f%4p725;%)>kLyr3$C4II(%?LB-yL zOLR`VMYQtEzDdb{EFNu26mic_3LZPj{jg|N_PjE$!SOKRQa3~SzEyUH7&tgqvFnyy zs3;tCQN$bOi2ZV^h^%o~Ok8zQ@oU4LL87k({ml^B#J;t-uT}QPcJkiS0&1&yzZ{dW zF3E~*EziV?YAw^1ee;?;=g1xiUj-@4v zU)VQ1*Y31J#g?@-_0$|Aj!R*~o#Nx%8;VV3N^?Z3kLhArn;T+gpIFiD$u&`>Nv1Lf z>uJ@FqlyoBKHekmh3zV^YNfv2I7zvFCFQv25|F9H5TiedS8=BV`}UO^ohPiVs%C$% zkJ#>wb@nLl*S3G4?1A^q`(=Ocola*~lWu#MYQ)Pn+r^lu+e$3jbi63S`(9UKY_vH= z(NEuNk;**1u0)fMzYG4{{={u@Apf?aC)T(_t9{CzE+#!yeBZa>J;nb9$9t)}CuEEB zQ5wa5))MQ0W6k*@hvy4kXQGv{5}(3Ae!@q_oxy-TQ+-IHr$Zw8k<8OJ&e%GJAA&%5a zS8|?x##-ilT=pzq$yL_Sz+<^$LH7U=*Z-xm7p@mPKaXYqvCsGT98~uAwKPx33$6!T zTh2Q9E1Ia78l}Xz*Rb=7jjXqK`^u>~4y_eN7FCuT1Ec)6O2(C|DhjqNJDz@==oU5$g3FhV0 z-M*ewYBA5n`s6h@@2jops($XCBNo}GD0XukS?lZz);Q-A*KWTWhHBO-=Me9keZq0E zNh(zQ%5h~qa@;w`3Thux*5ftU2ds&SyRIua^Swi=k|%6$?I~1)7yKh%xOTHYInG>b zS=x3|x3adG(-F}lqMao}sV zV*jPy_0*57(vS^$3IkF*)LoxxgK+DSOc8n z?2FTSyna3JpKL&QWB&MI)rqn^c|Iz4t<$B{e28tFqFSurN-2dJ4{`d9Y z9{BBn-yZnwf!`kZ?SbDO`0at;9{BBn-yZnwf!`kZ?ScQlJn);{`^|6u*1&HK{MNv4 z4gA)?|H~RM4z7U}COk8#w098dRx`npH-=aU!IllW4zI$FKSJP zqkXGc_^ES496O~p#uut^QD$WvI)Kip)f<3g?&O;k#hIhtKoxE~<%^cKwegP>K>a4f z<(Uu1j^!%jWj{LG_GV9Za? zZE|E+FFbI0DeB*G#TgH0@sKHIVtQ6cCi>fR@$9W6TBGU}_6m>?m z_c9#Wb12@q;D+(@$KkOa3(#~Oy>1$b3(LCT+o?;4p{|AJLOWtmqq#V|{&SO+8fSbm zuOBvOxdIpHtwcZ9QJ6D*20j_G3hPcChNCJ?$JOJP;8KkxdiF5IH)$bQvsxh5ujz|E zbOvr#0~7pCXIGzdT8QndPsG(>f1vpYYneFt-F z+td$hIFJuME>^f=%UAIrhBnfLz9Ee|p1$G}Ciq1t7uTwHT}5GJ>^!g7Iw@Y*6%v|ZzY=7$>K zrO0vUGowF#HV(#k2MOnO9E#_f&cf-L^D*j15O!^~9j9(oqsk{39SkI_{Bki?nGl8H z#qMa=*#;d{O;LZyOe`dxv|O+LICR}i)T~~FpKh!{=Z;=@df8;G-g6n2T`&r5{f435 zNqbD~+X!O|kK-OQz=;=a@x%Q|=runCRa+d;rvaTMoL3LuS+2!y_S>+%ADv~Lv;@u0 z`rxCZ#BI`9^ijLFV(Lma9NcIcn)I;4wl|lcb;J&|Z9W{Ak}bM)=J=Mk)+kb^;GEbw zXq#9MBkWY@Rm})TE*pt;>33w%E|hDw$7{QWV49yJilWB2|5`L2jF^Zc*EGQ+T{huF z*I>M0)Cqf~nxfmu>2!P?^$(ak9Gi~sjW^dW#*_Yquk2KFJs-8+hZyk?pW(;7ly~=#Ln<8jrL9Y`%C7 z+7ArCf%Dhm5L+uz%$#nI8Si}1pwa{!o=JSTC7s3oX*KG_QV)Rs z>oMe`6CV7ASjp26->mk=f@7mFZ}D`jK4d)_(?O;e^cLdQj^WtkX*Zl4y8?UYPshrw z7U9f$78vr)4_mD0g$JLmLUpx~Xj^qXnt>V4TlohLbg-h@GzzDFwZr{;H)6<$k(5^> zG0A^1`t7yBrGs{22;Lsn=bwHwf7!7?1)!vp8Oo`EhM+%Y@P0au=$iQCG#V;1TA=pPF(l6dL?nZz~| zS07(68rx3@!nSo*qLrByo*Ui-cT8A_IsX3WG-Uw7hrxK#&l}aovoVU!SU*0?1ZTeL zhmXcj!Zlk|*#7$zyzep{gPxnASJhrPY|>IJKdCI5P`oWFPQr9%8F8J^+20ex=s9q%+z@p6HWFv)&cT}g0a$1-8EwlhL(7p1aDA*B z4s0|UQ^)kh4;|)W4)szP-pLma*|_0=?{3)JuQO(v{((2&x?-g;6*^|G#NxfZ(7Epj z^q}Wd*%FT+;?{ zXJmhz{kbuA>@gSfix=X=s?J!rBp3rebjJm!J7L#htFYS>JKU320k!Uv(Qry9od3xS zH%2((1hEh^KD*+EZ^jtcYbuT!+5pFf&A@@V?&vnLHxBPS8Eda|NBQDdbhthirQU;a z_Vn>sxZfW2!tL-(+7LYD-5X8XbjM(aHJCNIGBJ%6C1%B$e_ln5GBKtSv#G>hE1WX3 z)XX*#!>Do$pQ&MfkC|EK{+LhXd)^v8W8N_#quoX3LW#vQW7ZQ+`1OWW#2GTT&8)dT z@zz@!cO%x*P-9c)n`ERvQ!JswMVP`b<3Z_X;=Vson>f-!-B+nblT>b(;yqR;q~Mqxl{Y54c`cu(mmD zS4S!ADcNdlf8AKi{B`ApXP9j^CZ3NuNG;8;%Jq+z^v~7Nl5dFf){}Qy?ou%W|6Ki<_28!bs&YAKK@4hnIh*-S6?3J`?J|c**Cpl@cUiWOUB^sTY-HY%7*P3W z@hj#edG8W4z-!B98Pj^&$_od{$un95^ODSb5*sUbFt4M`&zvgrxXhO`J9?#>9aOrV zp)j5w`t<AxCI7455kpOT zOmJ|rQmwt8t75|y7YpQl;w>E}K_vEy4xe~ofUcu)S zJuthCoo^^yX4IaIDrUcl3Dz+0PWz!4(>cJTMdE2??oYt}K2BOPy!yGv6MN9mwmY8})PsF@RgYRa^%OfcD3>D}4yqFKinz zr+jY$#a64OJ_+gmYG2!>6jT{=1FrL%V~*|mepaml)eNU z5AL-@;gHt zY51BN+SpYun3I-iKPv7Ez`YolFJ_LK{YpI>w1*~D)G)J5YfB$eAIr?n6BEr`zrr$e z9|F>yw2#$Y1=ed(_i0_ps*QFlB=u^JPONr5@SW2`X>DZHU zTFxK~x=dDu#0f~^uX%x)8R%zH>+-k*C8aK8Ex56+zV0>fTbVWpM^>wM!J(M-732k3bLv@UV` z4W%*16XlLCzp>`z84w}2qZo4y*-kZ{^OAi@EIIW`7$9?B0IrGLSAga;rkW~q9|F>r zmU?C=xkSvhM-ZFdTj>kI9Q=lf<@wpB zqGuWRTadXA3FjF8w{~^tm?z$B6s75D%{!YgmDEN<*(Op87hFEgE{} zDY-~vsUGf$XCIJuNrydv@=42mFl?z02Wx|LrPMpFfvi`0hG@d~M?h|99Z^aQ@52@) zkQV6)H3~n|xwft%KRE^N_Gm0RrqmFtu0})RyEM3$=MLShbcLi2f{GUT;Bsl4FxET+ z_ZH2?)gm*=sl^8fD1RDktWv?dZZ70)Pl2-iKEa9I`ohYjf|%}WKxfXB6BFjVfkwN| zz>XGl_U(ams5rnI1~zvFQ=LQbxTFy5bZ^0lG3gNY>>!LcPlbri?_hkw25{{Y58r1t z7k7>3ioj`~U~v8_DCcnkKCVfD?GGx5J>A@4g#BTNa(xL64jqPRWo%&RTN!j3-i5RV z??81s2iiT)flWmtVE&kFD04^ySJov$-31xM>N-P?v#w~@@F+k+Rgq&+LsUxr1dC5* zLuiR4T3W+?E_5*rh@C{E0EB*j_^EK zUJNhrf~Lt@Xwx?yq^u&iotqAJH}l~2`#eZZApV`0!A~uBLYJqPAiwTY&~Nu1*4%yx zy7g~D=;)I$-S!Ejn}xwj%ThS<;uTCw+XJsX>Wht&v@m7bCAe!44=oShgj%ayV4K}^ z$eh$Dfp`t~)z7MH=VC;^tZyTb6NS@6(9UxYs{0u!4MFsXkJmM@HiIdf}> z+S~Pn<;rK^EG59H6%B;dmeU~DGXy99L*TJ!E1fTI0Tmmiz^uuU;6QwE>wS-)`i6F5 zwDWOr9&j8UZP*3=*-20_`wcv3;tcA+xnOd$yx6oUAG++^3Ynt{ATOhq=wU5`@Vp3* zCY*+bk>y2c!CBbV<+JkQ$@-ChHu zdOn81_m4o&rP0vkN-=a8)kNH{76%)>K2faB!z1cdP~=T4cMTnJ@xT{wJ0`=S&01Jx za2e{=X(TotDKEU*KZd^7zJqV>CD>K3g81m033avkaIo10SgUs$CawvGeMaeEW*GxL zFX)IpVSC}8TMS%SpoQVCSs)#}3Bz_IKz!7F@YsbhQ z0O+6S2QhaKLDsmF5PPqucvzeY&uv_wP0Lb98{844JP>V!+nEip{hhvWTyPjn?F~fR9S^`cpqeOGBN;|T9DyUxc0si`PuN;5 z8=gH(0>5u%gtb929QghShVRuEyAExEN97HnaqF^T&hE$Xa@$MT?z|o1(Goho{{|g3 zq0sD$4Ez1R!qlzjVeHKN(BFIyxO96C$CkZ^g`+bdXL~l>-ya9Jb%0Uq94^7SO83Dw_9>K&PlpW^ z4aDKHGUQQTgUJQq(5=6|s2aT&Qlj=j#}qx0)$IZdnEDu==3EDL{XDSPrUkdNm%-=Y zLC~9$1}5XkSCyYY*5YPjsD2KF-Fya>$Loq!t?G(JwO2ru?v2Iqm9AjWDHXZ|?1dMZ z$Eb(KHV87k5AMNlVUAl(NZj=Z=3Os>Diw~wh35HS-6s>q-JJv9T9pxzC7ZyjNj>3c zy#YE@(i6>%%8Hi0`Ow{CJ4D`(fe|)|u(??xG-}&MG}smn=>uNDgcdiU;q4D_J@gW2 zJwL&N`Wl!YXbUrrUx7-7hhRX%>rgrBJZxw_8D2M!0uzrENPk{dWNbYL#S@Dp&&lb~ z`CbLFCTJ6saZ7+Phu=V9i+Uo}s=DZw@CM#&y$B|UDvMrouENy7FOXPeBb19SBQ`fp zgyCl@2vf6A7@_wG>QT>)s*PSi^4BtApvNZAJS{IgQ{O-{+TXR%vLa1C8&+Sq3Wb#~ z!hM%0n0>LO_?li$6gPYb71NJ{?$irlbf%nm^sF(6%&-I_e@PISWM(l+xue2~qUq_fYpMr+;Ol{K61RB)S7sCpV!NPL4!2iH*XywvE z6!bp`G2CO|#Rk9u#bD!q5_USCgemqr;K%<%Z&*1s@lu-}|JNMd?UENiIq)Mj)0O!= z{ug`i0Tflbt$!mTqM{;ZG3S7o1Kn#k%ZveY#w-eAR#eOqBq%6g02CD!6J}JDrbR`? ztYFR}X2gsc^;^%L(mRgMxpVIQ-|yC~;#5)Hy?gKXUGGXgHFJLJ8Q^l@`#yhujX$(l zx1xD3yFXw1ci!)x75i`f|MTZ0-TLKx5ldG6at$^dcL~$}oh@~8gsOA3{_gv~UN6|> zX2M^u{b!#OR%HC!&#Wjn{=e)?ms^8=8>2tlscB52b?lkoIJ3t7KVSc!c>geE%`fM# zH>Bk+*M7J6^K{5&_|cYFT%T422-wFatG-|&y@ z{qwVU|FCGX?yuJjeaimJwHnMU`rDY4=%1;s^{#rq1Isdh*7M#w+EHEe;q@Fpo%gSM z|J~=YH~w|~bvtVM!{3LOZ2##@ozCR^>GR+1f$Pr03;bB;`+YLoA|W_&lA@d|{Eux9 zw9)_MqZ;*t{!<&k-p_dws?(V||FqATR-KjnC2Ib3?YnK?Ul;aY{@ec4*nPo|zWe>& z-@W#3V|8G-ZPtHk$iL4?Q>j*d!A*CQeVH{ z|JTLAJ}|R8C*D_jdY)4bj@hHG2@VsyI_w2A4aO99eBEoU!+KyMv1YFx>r`KWJ9SP= zSD8+%eRfWM9r$=Ktl-442iWP)`ERLQCDsBb4L`tMaLeEn!BT>Cg)hL)!q@NtxK-Sb z^MKU_w~DiCtISuv1$-8UURZ0f%c|*h^;z5l#u&Ui7)$KYzlNo5hs!FJd4!*^NA-mn zYFxpUf@6k_T}oK$aNaxGjcSZxE0|tzkYKtI2iO8G7uUeyZb&uO!8Wjz;7nl)7*2eL zGa#Sv9^55-1s{PI244zx6Ta$nC_(ix_-6PY`KEH$5~GlY`E=mKktfJKd~f5nTIGq) z#XeB|j6J^OtEg*gw^QXdVGHsOj5l%`K7=h`LBXY-o4HHnp(}aaRk>@ht;m1)0A~iP z3YHT&jrf4Chy8uKrK)EGw>&KQj2d_Fo?xqyQ}8|b`Wd(0$-mRNs#HiRrK{2IvC5Cy z+CNh7!Au1AQhzYP$a(OsajP5Y;B)Z&@E@4n-B-7%+%vdr@W^SdwRL!p=kW*Dd~0#N z%Htx>jP>CP{IvJHMhD&%EG^=XSl~>B7CcdF3~@u80w=#wedkuxpsol07dZpp&$D`= ztZHJT11}H1!*9||1f1JJDB#jQQV0^*JVm1N%4*wv&sBy&GJ1a%yb+P`` z(49&X!ymH_kP~V%MOO<0r z%^W*kK!4lCEu0&<)t++c7G7Ja zj7@#7_9N68&H%fR7sx%Z-PQ6K>kvOM=y=5*SkJ{`o%$Xz$J&^~0p`4XeiI$~D*7Mn zMXqA*1!@hPHW+r)KjHw+9(%%ehPgTIH3h&TEid;?w`>*HMb-9hh$J*X8tgV`ML3H*y!NldE4rhgZ(=5yAil0hUmeI!H7KDz*MLmvYhkKYOOl4s>#s;`JEZ20T%d-msS4`h2F z+XLAi$o4?C2eLho?SX6$WP2dn1KA$P_CU4={>S9> zfyU=6@w>`{*?n|*jtTF_^+li6zfnETTApPW>@|T6@2BwMJ}x|5FXDvb0yu3*PVP9U z9v2i%WY?iBxrx(69=B~IzkS_`o41_6we9Wr+=5nIUAWZ(j$wTHus1h6>BWm?Sn*dw z9!`#GE!y4Q9BJ2$KNYIW`i|2$%-fB(M7wZ}dn5VbCr5S@9b^5T((JdrFQ3}bii6T@ zxUTJb_8jQRFE{jN|Fu1Mb<1}A=;%UD+|ZNFkDId1CWVjmAIaxE+VR1Lp**>f7jKJm zXX}UlJfxl*yS#Gdc7`QfyQ&XY^=`*n;Xbbq>&q+030rx16uaE0#nXd&u$5@4Uq2qi zpZqFtnH5tw$eVaWwP9?XRfm^d9l`TEi6-%NC(d$Q#_!(D;rTB;dBa&RK2~Q8ck9$e z7;tBHE9J_Io0j1LVZ~LxecbKA9NZ~_L&gXf?K+Ed1}xz|)o1Y*VWqQzXLDA*7Tjn< z19qcsTx|4kt~x4$?+zW!O;+~ek!e<3fAKPQkFn!AIsExq@o*j#)`c%G5*^&SZCJP6 zkB<(S&GV-X;E2Fz))%+oEYCsQJFF!8D>FG`X#_8;JD5{<592Y%rt)R4E<8AG68n|v z$$QHFluNf$xbBTI{IGXN4rtz!uN0fhCPN!?TB9Kxv!fgL_~gU)Ejn>g>$aS0%pi6j z?8G$JjZZyo$mR_Lx%+8_bx-_-ub<4_V|W9jgSm4PH@;VA5|_;5&IRmiaz>tx9KLA;Z@%Qr`gx*d zTtIl@rEWayOBDC*70l1;&*OfVX7J5s!F;Mh3%);Y5>GxR+U9LV%e$)|yXvO1z0Yud ze$R{hFP_7tBir)oAWwc=%Zhh5Zpxj_%W_c40*=e$z}kf&Jg0F0*K4wlIW?56>xmwG z*?xT0y%}#<>%b@1&Exd(BiN{B5ANW&l-DT!Ts75$`yN=pCpLI<;p91NxjcZ~54Yvy zQ)9V;NjTd(cyU&RZmRaQ!{`+pG`JI=zcP`H>V)uqhn4)Msw2;S<;kU#uH3zxCGUPY zjX&oUZSaC!_-=#)4;j&!$4!)+?B~h;EoZQ$(w&n_OyZ6!ygBbJNAA3NGEeE;m(Q>5 z%u!3L^KjvlFVCFF5mGaUw_C9LdEtr259g@v19;HHsqAX$&Sffx@q|MSxqg?~62Ao; z8##q73k~AT{Qdairct7Zlewz1ABR;K!;>Gb=2I>v>}ArE19G+H-QWCp$YyW$ z9O}*iGJ7B}?=-$xZZS_xXvMFsBY0n?=ygx_VfR#LPOs)A+S5VYxrjG^;}DK{7R0Mh zjpn586Zl<4XKo%ojVIOZ$o(Gl;`!sM@$RzW+;Q4WE}YAe&$ks_`3Amx{oP33H`I~S zi#u|`sslKE+B^>VSd3i)r}4OoU3hqzY23K$FfP4$CGW~ti${BQ;Mey>r&>3f+m`a+ zxi$QG(#^KKq*xbT?_$pn4{JE{&=gL*-I%cIz+mLY(6=TC_p`OVp(Ji|YbS1lOJzN@^Hm?@44Z}!F4C_mh&Ch`?fdVnAVGh%H&V2 zH8OVr_s-#-pZjvD4+A)1*HnHjI_B=VhjDXZ_BWpC$ye79pJ~kee#RU=*jvkA9{KUx zokMuu`*!S;)rB()6lW*j@%%vi{V>{@I~1S7-QQN>SyzXsF_^r*EtfFv#}^*A;KXiS zx%jo7+%V0Fr@iaLiSADPEwv#hW>_=0LU57L*FNG^M%d@0;4#5MYPyNWFEpp2&ka5r zOd{?FM<=YHFm)UUO=e*pHNrN62PLqb!nA6IDORoO_S%M#2&<(g=5G09OuOUE#I0 z!i3t22D5hWs>fnyQ9P@MF1xUudT`v(@D)95z2?2OVYt|nA{;QZ*umUFpBfxF_V4A~ zz#w|h255>4KQ7weU7+!+0q+QY7)c*$eL)2Q6DLw9u%w7VT=ViFgirw$KBXz2ZeL8rSv0 zV5%IiaL%eec|Fl2f1aKXI=dRMi{N#kNi3YYaJQvYOgz(HifFoQV?# zna*cv2~0WIOw1|(;|)$)bkGfmt7uC@e_WXO;(F1I2ZwGeOtPv?4Sp6Jx%k8u`pbI? zWPmY8oP_1IRT*SqmYYJSI!HKi=ytswWH%@2X+24fp9TFGGawP2B9>#=HQV7uxwaQ2p~OrVpjc`y5mRL4C0_OJm~gq%RmiEg+s(H8N-&|?Mx!Kd(LUw>-@^wyzgZfaYSph@nO+J%Ij zj~CX|08SX3ySwCwJgY}uKua2YJ@{F0!(ef-pZHkSUvGKZO7zf8p!Y5{s@4{2)E2%L z<{v!vUc8su)qr8gj0n*j7kif@*TF=`3)3D4ooTU4108YXXoS=_m|V$gl_y3nOWfk; z2{-K{H6`_^^7E(_u>a5&mpK)}CQp*}SPx!USbW11;k5_I9E%-N2bgIf=NA^cl`y_j z;$$9!9yKexHs(cW-iVJRj+mPe_lG|E4)hld;@;DFm&&(`O&aM>dT3EE{nQ9L=xRLS zYt$a<7wgIF3@zqlpoWG2H58Wl4CoPCkvXV5EzZQiVhzYa<-?Ik|IS;XFWnYDp= z59lB0IVwvn`sUE}-Us`@)`OKtkC#~E>;~zjTFiz3-~4*^B;*2OZaYKbhWQi7JVjit1+=@Rp46K8qvu!EqTiukNnPp3OE1UV z5X|R5?!2r2RM_sma<*D}{Fb1$&|@)^157vO1AuW~GiWt_6SUCz1}`tMlG*|HJqo=< zjS={G$#Yxb;-MdpT*7Pv%?z2F0PekP>-o?@H>AE%g+T!<9FEuZHCPL;sEYqy5A4ixIAbBNoMmov8YgO%bi38$> z-&wHq@_X=F?b|YsK%F7Y!}bpCditJ9>OWBHdKSvsPhQkKWh;#sFHHKS5ZYoGL1`84QST&UWo(mz z%9AKPJ)i1F_aozJ^|se^(a2cI8FZL(4?9SA!_%qMg~y~%sHEgOP)_k0lv8<@BbvgC z7E%0T;;5ohN_j8*YO}>5RQ}sK+9dkxd(tv!ldJH*^{>%q{}q(z`GuZDCX$26Z7Mrv zEFG}1RQjguqcx%PXm8mkG*CHA`C5jM#kq^rw&n^de>RBbTU#j`Kfk6fFQRDY@dC=C z<0~m}shP6f_XWicJ4P0DH&Dxok16lm&s3w5fwFW3m4W`ZsrA(MO4svxZOb)<6#bL^ z^vwGlHRyAb0^(QG(_QoFRsZ5j!82*J=ImCo5-sU_YXWFSys46TFr4yM-A4P0Mo}fF zcT^%YiK>;nOL3w(Kd+y$^7iR6vXyi1d6P@YqLRuE3(<{le1_VbyhMuv57U8mx2W5^ z3`#z^fb42*p~xwx$s_kCiU~H*1L1$a-YiZRI-aHD9+A{F*Jw&JKTG+9%dB0q5*bzB zMe$27QjUGk$;)mZU0b-CKKBixcSUn3%Tg{<2dht%-}^4j{u(2?&56_|!9~e?XBRc@ zA5VA2T%!RmztOIQTFQe*BWZmRkVfAq)tD}SPM1CpuQ(Z^Kd#yvVW+J_$O+d?agL{V)yf7##-w14djx?5{1 zyL+@r#$GHJxo1iEZ{ zjjDC2OEx$CD6l?}pS6(^H#?F{7nv*T(-LW`;|ZGQ`GIzwe?j+~C(+u=7ii}ZGsQLb z3E94SNJ}2)R30{pC9Czz$o1-Hy4^mJRu$PxVU5pI8=gp+&5ae?4)bYY;4Yd`=qPnP zv7g3&dnp|MK5Ct|gFK=e(YfuW*-^6naYg zW}c=!%_GRJ!De#3W}<8voJk);Hv9EBDXIj-NtoP z?8R347JP$>KG;CB97-r|slw*gOCj&sJ88zNoXV$BOK5z@Ewp>}MRK{BOL-Tuj@qB}fZyO~N%vQk#G-9c{i*HfdNm#Fv^Q)SuG6?EG) zm~xccOQ9PQsi|8U$>29C6&Oct{JrU-c^M@=>KdhvHBx$B+C}SZXVABg20Hd62YEXm zq?F71snEehvR<&BN_UQ>>jkrDhtW;iobwEI?qsUWN?AiI^P4NV2fd>P(>_wL^+WoS zZmPU!bdo+kc}I>}l_)9e99d@?D{iyai>7%Nc~pK$#-Gb6J!Ce+<#KbWWd6L$h>`25 zO1@`QF{m$Dmfubj7Pq8|OG_yJ+Qy_5NvG1qx6|MY>&YqKO}fx<7?raerZ0 z8x9^Phsc-oq?Jsm>GzHD=ZmGvLq5{*>!)e&`du{F<_wJ-AiC|R(COrWi7h2FNF>_eMiGD9Hxxu zyOdTZoI(d=()>V~9}t<7I`xUBCovakCn%U)uUII3w?C%0lebcJn=#ZiF^$H4 zYM}I*9za+0qC;ORiHhFMqL1S*Q>%9`$@R$|>b>q3B`kPI4>ymeT2=#pV*JjYKKzYY3Tz|{s{q;V4E;0URW;xlu)!(lB zyXXI%Yoc%b!!ED7=YMVJ9e&-dYd^fL$bp5oIg{IKP(_l!<<`sKUJ z$I}x|EBSxRx|H4cA9~PO>*4j;rzi{#|_3e`vUWzxLPHN`AW&?3S$iM|&LIGWW;&g|+p5$-m9f z(LcUFQtkJ={*^+DbvcX%{qpQz?ZW3wr@!av9~7Z~w8ppQ)<10e-5&pyYl-?k@iuoR z{Mdio$N%8dZ6B3BFxBbmkGU#;UB77FTtDp5Jc{`79`}v?SnPm*tCPQ-H_)R;j{_gv z3m(9I;YNu%y#D!rAwd@VeSLGQ-~ajgzj_bO5X>SNLU4~@2*DtN?}V-^*hw&zGgs|Y zKZ7A9tCK2Qc%#8?m74?02>uP)v|!}Gse&QJ8sPB274<6cyUXT6uGSaE9x>&_Hgtn%l` z1#qme3EVuix)EdO)L$!7T%8*LU%)5e!@*EuFRGblb)#l0`s|K>=Tvq z1P6}q5o2gkBR<#*tSNNZ!Jr~%z_KDnI3G0O!3N`;$bn9c%IUx>gJ%WT346h5g1JWy z!XCuUXScbkYY)B?EG(GQHPw%(deHC#I9Qwm`HMJ!Sw#$CFV5J0b{-wh0FE6TFXFB; zvci+%S@;&3)`$hTV$4lgQA|A#b<#q4_{0NpD`q1EiaSv?7j0fa-uO3xY zpFmq4dqKMy-yuF=YLQD|a+jSo(P4H0=0hMC@drO4<~dKDRAU2fA3lQJh&gvwsiE3QTq}0Kp%jwqwh(bNiPH+jaopzLT|(RuorQI53v_C zuyGwTCy>j?L(CljLyf$HjyG)dNz9|xG3pV2=yg~#D0rQ6qj{$4fAkC+w+iZv2IM;O z6t-i1!~nA;(6bOr)C*=?Ag6FPe8!v$^ey;q`RJ=ECk+-H*I*C&8EOM|Vb%j;iCG}v z-f=GY0Qn97!Z-K~JHc`zudo+li`UK@Z>f2J*nYpimqQs{_~x&FhA>ni8Wvo zY6)@2>;&|A)EV~1ZxYUic>-S#pZp{Dn&_hvgxaPXQstz@a{((M)egI}1{T}s!-z%IQ zxq~{u?-uF_HHKadJ5Z09>44|K*TYu4A_nMDumySNlw*-)<{pSx0kHGQ2~_qZc-|DmGs+=K^Z`f=lq z6F8uqKkG|2W@{Ty4(~CHXH*wX^>iox`l1zoJ5iobzU#_!@{Ht^Llf9#S4|G>IiHRC zhH!fnLe)m#-UaJ+~Jm$^m zUVXXM%!XVp=K`kZTDGa)iND;N&c-c<@L1uOwL`qQkaK-Lp`FVYA33m5SOD+aA)3jW zK3pnw5qJBvglmml$xB4@y7{_sZ2i`oGp{&s{`)Rmx6vG~$aO`3-HH39jN&u*?D#{= zW^9zXh%JSoK0+h7WvVsz&F#x4g;9N8P{X$u_29$@?Ra?jY#!%Umv=Z{O`P19c zyyI>^ZhY5|tEW%pw4NpZ_Y~W z#}B<$^YFRCA^UaV^s{dKdf8eYdtofU9v03E{JZeZTT?jy(@?H&638p<-8tu+u{@`i zH&@P4gWDKQdaTLwJX|D+f&o<6Xk!hE-n7 z=7qZPkhpDp@#P}E6eD>k8p~&mIr)_R0)})tvY6I?nORjR(E<<;(Vw zJZFCgzHGgjC-fY~-VLX4Xd&Tp&j@onV>MT{p3RR}FXWUNUR={Ef_>ixaXYJiY%ygb z7cLvjr)w~qAApZdD8 znSEOx5WZMg>Mm@xYBd|)uHs$JOZd8IIS=&k;*o_{@Ii|pwt2UZO%{&lImKeQP0!i9 zwstVTHgV#aa~8AT-au9sbmHqWN8-c4C49@$pOZfN@zo9v+~DYPwu z4CJ7`zTC6t04`>>lyitK_>Nv4T%_j)b_#Oji#dslXs2-Bg5B6}um?Xgn#*0<&*5e1 zUR*cRo>Sh2@Qi@jT%_g#&h0Xhn?7I9A9s)D4A&97{_s4mwkVuup9|-s-+bRQm4!H?TbY0afCM)1d0L%3_J0KR@bl;=lA@ta9u z?6yCKw|NZVS;yLPguOTKKIqKl`%Pw-L*4o8&1s@zKam?d`0@Gm{rKjgvHZBMJ5Rsg zhevH~&vlIjMC=^Evu;Oo_hFvwJJ*9NO`gxAUWvYZ{$Rdsw2A}57xKr9;p{8=?XgeY z*r$9|PJ8Ra3pBIX(0di9$Bf_(!#(&&pZV-M!=F=Q=kU}*^Vt5h53ii#&8b}%vAw~K z&vb3i9h7i39`DQMeFk%Z9kaQ|hvj_rt_NE=jpFdF0c?@%$19gRa#qt&Uh`%QyC3)A zi$y2%h&dsAZE!pG_8-Dy(%W*orX%_Fo0x%j^z=p z2lGdR7w3J}jYofOCG2|(Zas7^w>`L$pLYx3S~BCHazlHz+V9LO@AYNdBMUfFxO?~b zp(L*YxEh6=w5*UF|rjWq&R+ zcOhS`JCOaFj^KzQZTQeg2OjXzmouKt;P{u09ON>M57wW-r}hSOSk2xXlh2LkJ)FiL ziVfj!cD4BA?s~k;)`eG96aHN0u~@e9WzCk>+_{|e+|>iv{jmc(CA4K-=>UFLqysOz z>&3fw4&&6Jb2!#!CU-S<gk9%dS&IRfFwiDkgx!f|Sa zx3mR+E9@MYV^zBw%$%-dRdCs0zl9yWp@)Vzm^#r{){5@8Ef_cp%Si@ssNiS8!h#ir zzB0H<;a{78j>>{?crSA4aVV@RdyX3zu-xW%HH6J_m!z{5Z+q*YW7>4#}-WF?fd-= z(D;UaJJ@IFU4wfM5&jYS#b5}9h1GzYgx0%orC@7CC;gys+h_HoM6+GkQVp0`;Z!wX zGDXiiUhah^F7%`Ib43Fi{4sQ|g`w0M7cXzvmJ}eoYE(S5i}5Oc(?Ww>m|yMjb;U@S zP6OEeb~%o#_Q>1-4Q2-jZwuZ#UbsiC@YV*=>4l!SrcM762E;^S8h>VB+jSYhIPnK;Id9)8J&GQ4LLE=njLSO&)nbWlQfBEn^Vo zS<^uD=E2H?>jr}?I@+QwUqR)_=Ly4)xI*iGvG@U+@xsDG_Z#e`aOf8C+l6(7HnBXX z1W_b{}L^DZ_FOR`qCpXk3oZcMm>CfkgCq8 zkQnO0`Jyhs9!p=sybIJy{G%qt4B&MKQAJhD8$C-nT%1qsH?xGP#%ziUf%~A>uG%26 z!2APk2a5s*)E6|gh3`cTLZ{q-e9d~0s^;`ys%U_|wPbko6KOKdcvlMbzJmFdSU z1<^2%?^vms0d==q^yb0r!^hyKWsZa}$(0P4iy%EfgZ=~lUDib(LVl>sx9DSQ#tGAZ zbZN3WF9Guc&}*QHjy{Jtp(bZ5RSe$Ja}j6JzK-7_eWv`fKFCRp%=$1uPh0!||AR-T zqeV&?&(y+wmv>N@_QXn;$P(Ti`iU5||;Fzv`w z)Tca8FQ=Q4_(YQ^vl76OdrwF~4;K#pQ~cv5OA&i@Mg#H*^D*eG~Pqw}aMCxkc|xgs(PVPjuim?FtN} z)rz?iohiD%rO!}E)KQvxI*XRS-$i?m#L=_i8>mHA7`f#>Pespdpr9PWWlyS14de1D zowv-Tj_35We85vWbC*)iJzP#Z58R@u&4-ZJfc@mOFppCCSs1lhwudG)%%Cpa zlgahqHd@uvnYvFZr4&4sTUk6em(tVjBn`aKjFJ=f(4f{vN)N|})H(hgt$e+J^5-wA zyc+qCKE&>$LT`-}ulHxkDzv=fyV_jY7WkPibT~@+j?JZt-q*>Z$|2ggqO@`*BbidX zc2MCa>!@PR4b-;9M)I0^oaPQVL2*T%Qrej;ik0)$YdoDi%FHA4p6_T}=xYkf+C~oYrx7cg(emkgs6dNRbfsMu^$T1{haNwnk1hSEgXqTh zw=z=ZjbBI)?L|9!=wS-G_lTTEiblP+l~TZ5Sldw1O!iBltclNQ;&c<``Y2OnOsOzh zz2YI|PkcvLf<^1P?qw>u@Cuz=mrOQ?meJnTM``b#bZVI|kCJ0?A{D3`P4A|gD=)0S zP$|(NPVD`a`k7szA&Wjz^LwUB>K!*x$CacStwCBNby4+lN{w|?Zm`_kG(P3|N?j>D55Jao(@6)8P zDOfXxqvbx_#K(KkE8LG_R%}fS5&r95_P%}Pj^43Q~A*yXq3rC zs+n|yKIeH#rsfB!OqJVY$a#yJoH;_VYw{~qN*7X+_ok6?^|f>}_#By?d`Vjd8EC25 z2HJJ(8F|~jrtME1sN;tD6h31EE#51(+3uuCbe2MbSJ307_h?1)O_cuh2o?HLi#`p! zN_}L0#@V4Il!9$)D|wc0q1>^z>C4j#G{NTv6`PPrJDUj$y*ioPCw`3Y*!bf)VIYPsqP zeeRG(o(%%1;=)vFHavtzS$CmnSBKJ#)`gYBpAsqT(R4b$Jy{Mi~xl(s7`R{x|t}UfkZ1JIi zsWDW|u^?5>s-~Qk8oH(3LEe68q+MmIES+?V(iXj>)Up{gT-ie{hz@|Mb3nQ!Ib-**|^mzjEJDduMg8 zhZP?F;(LpriT^D-s_icR)0!JE_xZ=`Y4%?EM}GFIef6h3{>}&AzSa5ZzQ*naesHfN z`YrhByT7iH$-{Kr(*E$%U%&gi&t)6g{L3}}?zw;Gx^q@H-M@4HpYM$cJNt`$_0#^~ z?EieH)0+BW!>FhVfxGW$FlbSWBUph70{RGfRgRvB}!vfu{6oP(zWYYC0s*H269iWd)4o_U>DIahFY;KRWj;_tls24!)< zS~~ZstZKbNKO4`3KLryB&GMl)7pj~zbaP{aO6kB%g7<@#F!lrw4yF+t9yo6>eU+S= z=r#*?2o?`(fyV_y3|0~R9rVM&tll4Bsj|o5A@PT^;VcJ@%yiJ%1_KCYH-GEWI7W`7R4ey~j4F(cxf_2ApSOe>W55*onRV`Ew7waUa-x?o6g@9U6zN^tp9Rreg+FJiOwjgbxvGI(s{ z1ab&$IpPRSVKAx4pP7=shzHi1n)prS_^}`21`Ta6s9@uf->?~MJMs+NGIH=cw-5Fc zwu46n^NGB~{m6M}uOrv64`viV#~wBzZ=n?qzk!{uJfyJ7=fk(~G44n1f(3^EFw+5i zIQVg#3+DtEj#n_xh$Z-JoDXZ@TJYDWYL0+^#d^4geW7^`8^PYfPpEA?gYUrfqSkQk zOS#k4=aD~%8+4VCi(vBMd+@HPo3*3Ob*OdJ1bl@UVTJ(K#7qb<#mFD$vgef!SHJ$@ zi`cblJ;Qg%pK8XpRePY(jQf47ZBgs#!{`*nf8JWPC%|{$hLH#G%af7;st*ufb4!c~kF_I;?>hpk9!x@XfCSJiw${J=jW58y-O z545<^m*8jk6n%2ys#Mi4;NRgRY(+B^JT!w9!nE+qo9>f>1J9Oib+P9Jauo=Av zxrKazP4Fvtb@W!$8*&5O^_@0Vb$Q2cRatxF(1Yi=V zYHotD$5{|t^yCGL7OTAmHsTu2(qvSc8WTK^eu`_PG?ZA8y)FJ*bgW-_LUB&W| zxi07I2kIHogU~0w*FE|ve1&tv_K0cM)X(VMs0H{2{=^>WyYNBxez|n;De??;WoQ?! z<^X0~U@ijU0DtCSS4?TZuLo7+4Q>@U)A@tl!9|t$ z#gS`vuCEGml);28Gju%maZ%pAD>rX&%*DM&T5`01K^|jYj;|gs!Q&>swp)MFVCS@~ zAlocx%!j<}`KWbw-g5l2UF6N2{Bdw+?su&bJ3K1I?R{!-nspsETiufH9_h$~59H#g z1AX|KStl;O-GoO?YQ*tv%dm~UCXe+e4jpR6!{?OZeP8?Xq*gYZ*vCQG+DTlvat|)= z-;k?3(D1%i!#M0veh#@^P*z3`)x!U*uFlOu2GiL zs&?TV+bZ$?BL%to4tFl@+MYjrY{Yr{x^a+bpFghEgNwAU%G3L_U|pl?Y}sHqzs=>$ z%Z4=Phh4kzmX+q5ZZ}d`(RRFp8}l^dfjn-N!k)?XdEa9TZg54OAJB&P)TqPddRz15 z_v6_6OB)Ue?#7w7J8)`_Ts&Rtz)`|VH*|7ihkQNQ{eUf>Sz3mJsyE_`{hfJiyc_3v zFq(^;w`Q+F!W-+$ao!CMY?`A#uaBwBtNQom36+NOrHL+lV`vNRv(A;ZkScpHb1W{ zF^tXjP2vJ$8*}9itvT&>74F@}k)6Z6_(}W_4qDZMZHg+KleGN`DYQ(ADo;)wM9mg(lVOsyr z4l@fv7PjZIFAMRY2+_0OZp{O*^ShrA6rPmM}^{UGx>Q>_4Mow>Ai8{THzg<0F5Pi)t4>V%fu?rR4gGOrQ8jVj06 z-nL<%l))Uft1WlEs}**(7JshYoA=3lgk*x8_}t@;^WkwV+z#Z_*jLHSQKRA zPv`9Hcemm-RVs6_9PM~k|H0fZ!G@Esk7u1nYo1{3z~NIma{V3>>-|l5!U!)muj|U4 zdr#&ai6wbaRcqdQssg{A-h|ISaN=vi_6~X8k)3jl=L_aCo5HU(@A0zXla{{R_RL^* zpWA~CO&z)9%7GlQzZ(xIkc%6odvNI--$>wgIo2-HdNfuEB0Hr(#B{-W(D>k&_CR)Z|gZgo9syyVQ{nUU%liDib+Tl)jp;n8Ri#tvSH29~)J7 z<*?B$_h<(JiF@}MLK?oiF2N1rOnrtO{B*sMP%JNM*o8=7$W^R3z9oQ`dZx8kC= z?fCHZ;p}wVi8HxAUvQYk>DL|j@#7ABp4)L&Lk(Mw?aDpPdhq6w8b0@>Gf&&{!tQzK zEOxgWz~hFA#9Md+9@V!!SNER8opVdR?HSDNdM)4=yIb;?;jK9`au8Q<(VOeV_u;)6 z9oVIHHFkemTY7Iz{$T3B9yqb54t>z}H;bvfr5*yuj3*?KeAe z14BoiV$_yLEt$dFE!}v}S$m#vv>Pug<-wLMXNy+4FCQ;il-)lx=2CB4@F}Gp+n@5} zlWtvkMNUuNF|h|1IwyMW1`qD(V9!nyJ8*;a$-Fqf6zn$mL-4QQ5#cwibEe-^0wWkA+$DHKaF4<< zYM@0e`s*}Om{#cTjz8ify1o@uo?iGtg4T0U(JzNi{DdpEhFD=5!P`H7{z2uS!Rmv} zgzmHG!pDndyQ(t}zFjzM?Q8S7gm^9!hE&e2a=g&)1?vf3T)0P#aF-hJ{D|2Mn>$+Q zoWEH;M%d_J%nDF-qQN|ZzuY481x|_Y8VKtQooM(<<^pKoAJHM!emY;<0G(p!*+at| zOto}Afu2+gIkjYx7wIOfWl9imeVURPL8HGVQ5LG=rGWjRAU*kRE~ z7Omxudg$AOZv{Jze+!27pb;;A^O*%XRd0LW8e8}EeLDFcjttopV%X(0Xo_8 z9efRzT7!MTxq~eQ=L`*Xk*v9PGJMxHq57oqC@(07*^4|>T1)JS~N(qx&< z5Fu=>kzu-(waO(USGo!l4-II{9KhM3y$&sIsYT3b0B;*7n$2LdF(V-!EIDdb>O3y$ z)e6z64vQD<^?3LT{4!X2(SjBpc7(R~hwa*o{#lqyLC6{Ck3;txye)Wo#9sLJc$`IY zSnJWbx9D1r(|QZfkLR&}U;i!IM)E!OH zd&y5N7=7XA4Pc~^yE4}!9(v;FRi1Y*ihlYymA97OscN7vmYE^X)8}h3mq0K5Q8egD zVyu;WF^>SV5cKY%oeh0%@hR4jo-h!3rjYy^x=#7#~dT7yOhKBrZ7^Ej)jzB#6web4!;Pqv$1YwSc#3X)OFMrHoXo|j! zStoKn5}zTL)%r!;mr3uInG)I#AxE^BeKLAYe#6a_ng-~fqi-N?m=z&2IS8`~WM+aM zvl1RPS&G>OTFF_JRmU6+=*Ua&vz3`8GS}gt)DGq>MC}9Pk2xU2rM?gkoI~;>zDIW> z1M*4M)S!0$sJk?kAIGc$^gz_4=*^4$X&U%QX0@n&rJBq-C=%0zFpC0vVHU_BnPY(2 z5W=y`>;(09)K-26wCKsQr_341qXDZf`K`qqk=Ff72xIT3&M=VA8u%773nJw=5ozU+XrT??mS1^QZUH^^ z*hJN~I@8K+7iiAcJj%t%4`|Dj1w=K=DRW&9(Y?u8MAx3uSKVvUyEITTrn^(UBd6(k zrG>(EpQdXkVyM~Ff=b$3Go|YRVLijTlQJQ<^1&pBGNqymIjk^M0^_Dpg^o|iDJh7` zl{Qt3TOOtHp?cCxd`)BC-l8#AS5R`-0TkmliUxbmqI%JKS`;OmqNautyzdlQ-z}je zYxYuW+Cc*>wo##SC+Y3>gEW2PYHD!p7**FDB8N_9O0@r0DzK{zT`IecM#e3qTB%=Y z&WQE2?Zr+?X;VmPS>Yy4kJwIy!cJ4US_PG(_Y>&R#7fF(%dbRR>eB48Yv|Av(RrSh zMfN%SQ|5R*U0Zj9&gY7uit)vjyf0$NS@he7jm;oy-)B@mJg1U4FsEX_E`V}HS}3)x zSJL&VdhN;&m6S)Lu2a>`jg)Fz^3!$UWP=a)2tH4l znT3@aRVPz&vtd+i-vfG+R884jqpY&QG@dMaou%Q?7D~*iND64UfU4!6Ow~ir()p(s zw8J;2Q13chX~>9YvdQ>kmE;p9`k4+TAVPanNZ6`LNSdpvY6 zjS${DZFx)5x!6ygg0k$(`sp+fyK~qtS^KC{kBZsS|aH zjD7A?q{ASZn(H`?J?2AC4__hYHp3#?LsZ_3FBJD5xh4fYdRKLV0x?DSv>Q^{N8!i@5 zwk2($YE{x{mg`44+u|Hu>|C8DPpwRKng|=*Kew{1;zMn)o>TZxr6hvRFGAMEP8hZc#vG*Q8QEl7$ zFQO>sggIx-IY5uq3>6U-6%`c~a{@s`#RwuGNkj$22xi5c*eclFf>}|_m@^v{6Xu-W z_||@KWN*(sr(XT*)&EvOmA6-~o@<6N#~ka_J?Hm*lde`rVp!WKFN|!Q5ij!^^Sv}cB zW2%0liJH>N_U&6Kwn{9unGr|#Hk~2!*u`}FLO6|`-I#1wU7!x-574Utd#HW=LdxB) zg_Y%X7Sgh7rIfQZ$|=RIg`H0eC8KS6s=X(fJbvt=5w`2-$&y%FIHA09spLb7U)h!N zSlyuA=HKbV_4zbw?Voh!vJaI#Q&?FR9!OO9G`)JXmBJdXq5XUAQo3_0m0x*+0)}|f zP1AT9-MN5rXMlk=k9|x<%N{5E=ijWqLy3urS=aBM_D#yV#^j3VhCkaCv$~(QH%FT`g&SC9wfT3SXLX0OxqyZ9 zi~ghiXYT=0+h&{HM^@W{HS%T8HS1p1=h>8$?EV7-0<<=^trPLvzJWWmH|JLQ$8-LJ zO|tus?ejz3_o}Ifv-`;E_ks@hnkAjyIyw8dv19(pj^0_?Icb)*T6Uj>R~}Z!&+6yj zeHNUP)06w}?euz0(%R#6@EY~M@j_p&DQvgdhM=9KlUL;KL|`&sw! z+5KtaZ|^^vwokp^x^8c^kNO*<5`TT}KV5GzZTs7P|Nh*c8#1%oU8=DCAN%z8_U~8D z|Lr`^j%k^_)?c6L5?=4O?SF0aQuxiR?YmLvo{$ZGrR zXY#b+ImW-xv9mg!_=*3vkN3uHtbTnS*Po-?T3s96Dj4R&b>fBm3Ii>CPIcKe{H?^iZu65ZsH5L=P z)cD6bx`l5#h`CHy^UwN|$H@1;+A}<#`0{IxIWIVEo@1V9(^!|Gwh8!9DmxyfGgjt! z4(c3_V0 zTrHqYII=-yHNk6w%>!=*93D2P8Vz) zcs#IoU@gHI<_k1ZeV@TgvRf<4{zCV%kpdCw+6z>i|dm#y?2`QxDD>|G+(hiCpe_RAr0f?zh*%r&m*Fo;ud} z;fK_*ZDs>;42R_GtG2ip$r82o6zjemL3gGWYtm2Z^(5ce#fH1*-?y-oB?+V z-V>ZXIB;kiV6UpXA6K;kVs3v@^9=ckd_yi36<=k<4;-qq>lT$Wh0itkP^=H$>ey@K zFPK@x4s1E%0bg>&3S9U2bp^EGTDyK(#2RBGN3aIm2Lp|>k>Xlf3pN>h zfxSfxP=BC3fm|zU6Qe$Za|_)7b^Vg}=nFLh%=K!|Of_ye6WAM!Z)Vt~o-O1Bat`r^ z_cYEN&H!SJHs}v^2xFp71bn-w>PdjXMlK=$F(>qk_#l6C-M~I9=MQ}thBiWYJb@X4OAv6JXsKzevG{>gVGfO;ybY)B)5V zRc}JZM*YAV@!rCILo)&Sj&~8}fxX8Zkf*qh_?0iSTWtp&54_JNPr0kc6LCfi5Le_7 z@&RWUbrkn;_AoxUcLE zAzSvo*q7TDD#pDdI`j5!He5K>oEs>voRU$2k56pC72gH&052c*?Ptw}=9#jSOI^-$ zYZNOFda_w`0I#a-$ZL!C<6DV+d6ZrnU4G|pARS27fYYR{1Z8yExG_eia*BHeI`Udg|(-pkzUMN4!h~Oy>;;S4# zjPrf<;0xbE`H#*^dDZdGJiSbPcI!HVM>^gwG6iDmS0c_>bpO2Z#f<_hkPU z!Q4Ae{OotlWJiNF@AnwRqpr;40UqJpEp0JpE?vf6Jwn*AP#~W?7Ra6-?YV8-61H;l zWZQCD9v2bB{s;Vc`nj=uEj5_$SL(%Gf&O6|mf`_nAvL9c#zMRb$EMk|LvJYt<+$JcHXWW^?pYzOT)6W~Z zRB|s4{Wz6V_buib@tgQ$ul`*7YdCKlGKatXv4-8&59X*6li6Lo#M7@w^Qe}-{H1v) zM;}7e&_J}#$EC^H${W*o{k{pRtK3O)GUkp(>6c_g>F z63GphFXCdpL0tXEO0GQ4PjVxOhjiG?^>++rZR4TbDF0aQ-)=6q@b+iRozeW$YZmX0 z4&n~o+VYC;LwMDf5MCty>NlP0aUSuIPnX&iy?qP^dJkY<-AvW*{YOJDu3u*kX9Neb zpOX{Y9CKl9`>|YcXjQ(HF@YP!&lTQ&I&b&&p6#iTph`;XEA#=S;T|gf_UfH016`yHp&(Apt6^Lhcn8!0M zCUU|%H?Dowm5VG1;eZHF{;+l&M`^ZIJ@5k4|oq1aC z5&YCOgky%gux_J0U#mHsJ)SS*#T5qgkQ0I2AZ{L~o(|xi)5q|@{t*v95e5 zelR!q+>^h^c-FHza%wJ*A84O*hD<9GU(u|;c;w`vEsy!8~*UZ!RtdLK1JL+fd$oh z%e+1veGK1Xov`M5^asCNc(DtwtOjt?sX|nHuro)rnWHu)rfkr@#VU(Wbpvz< z#2a1*&tvi47H-^E1CRKpry}*p4XjrsrndU z)ZR$x5Q1kiyo27iq8h)#n3MdoxhM{uqXhm#x23U9wy1O0}?-gmKo&?ErE zFK6QxbRrHQ|KN)a4T+oFgoM*RFj~%;Xj_o%nOalek1b;vcxz3r{BtG@X-vnqwh^B)0i67FS9!cHRiynj~dPhU4718Rs zLjR~Eat3wax`hiz%vI(be&nJT0WW*xu}1VERNW9cTc}y!utj&kThp@jYw=P42+mxO zT#@}EoGa)g$ahQeXh+S~L61NMrsLS`spkxvG8&eCrV#Xj4EV0p~YPn0j#Vcn{$p^a^CI8qu74T5G1v_|fZ*ra)q@)Pw;2VDT@YoOLjOx0S6 zc}pEpH5tUeU59!)#kq}e`adEA7)?MFPavoee-f?aEKh>Z%;@t`zkS<~0 z>26{gowfdxwhHe%!M42OQ9YIdPX*HLN-@;opH?6#($jft|`{5@@QI8Lo9M$sIT zH8g+LU8-*Kg-kZTp;(VC^k{z4Vyg=rua?EjFS23Ju^C10iM3G=9?-3#*Qbb+p3+eFrN;>jX7fi&lLP|2Q| zG~nSDvNdt1l4XZd;JpJ>I)4;JEnGsL%g@r6svqc?K7rZ{Jw*9S9HA|QsO`Wiiqo2Vv~ZU%g`BuYNt4ghk5_wWQQ9l&*z^mnGR>YgST)h#E?Zl-H!(ucrhzO`!Yjii_vb z6}lUkL=H3aDTiYp(e73GmB|OcQ0dQ~Dg4i?^x&yMLK5{+1 zch94k6e2PpD4WpTM?`c>4?NqCiBh`Lx%eonKV|-C%K=GF12Y-`Fb=XH+x_+i@kcOdC zYATTWgRYcGC5x(8sLuH{#2F8%SYsUx+J22@o1Ud(p*Lu&u*OfFKTwe76@9X&Cx`8em0Ii>Ryq_hBUfdsg!bU^g}9dbc;?+ zzfH!SzS4usyD7D30&PevptxSyNTYmDQQj_k>Q(<64IjFZB1@kjv&nlX!Tun%zB!EA zPJT(5Ei#ycG<~FVH*-QI+zoHJOl4!TtQF1s3t=#pFHbmVa-#UlLIzt%e5_>3Y$qTyPa1kwUbDH{bH1%Bf zla943O;z`$QO_<%>GDEh`_p~tV5v9?2rQ*IuL!1nO;hOOM{}Cj_fP7oJ4(atODgqK z_EX@8<4Ypa*y8!J^aTFQk5cl|`EZ+Ld?w zGn!tcm)?bymiqHF?VB}yyplru#E<^D)kEr0H-;9|A5>v^9%X6%SM=y-2sxFHBj+P0 z=|jKqG_z$eU2;4}HM)MHqY0U`x#V{$va6DE?ME@iw%l5J-SZ|@HJp%|xPW@ve51Pk zGU(XO&D3YzB`Q<=6?ry!P0pTa6cLa?HcoS?NuEu#wNqXt?A*BdLd zd=Al_-j`_atte{x>@7XR%pR3#;rLZr4vEj>uB zw!WZ@bL(lwvnaasD8JI@=K*@xaR+Ixc~GOn3ADMnXooEHp{YStl-~1y(wHJKWV(1Z zMfzQ*zz-QTG~gnw9ej{_Ke|e{_luUsiO=-V{t~qfE24}TyPG18zoqq7+h}jEQ&hnJ zC}r9%qJg^d$|t*Yx{{nvNqyi;OD66io6tLSZ^k$3cO7^7H16R;GT{68G z{+O)4n<)j?9-;K3+v&r>uSDmKl#h86sdc^ebhSZIC8~1+rIggIG3nc>t(>p@qCc>w z%`IA9r=aq6+g%z~RoL|2mvivv_;2bjN9@e;zj?0{oRH19;rG_Ic@y#J;{O*rXa?Gx93|4SPimDp+K zn#9sGl5@<%<8#?0v~9C*OX9q!JZe9GfA;^x_p$4>Ir`q)`p0kQ+P7~RwOt+~Bdhi1 zq1kO*&6}$8#9W-Wwa>o(b#6{`j%Tl}*Z2e3*J#tAnoagI@VEW-xvcAh6N;*B))wFV z+u!#c?|Lw+?SK2Zs9{Lr*EiP_i_~tdy)n0nx?jx~?#R*4+JpJB=a$v4&1tsEy8iXE zvTaVz|JUDe?KQi}Z=bs{_uuoIKYteg$M{+O#*ZKL+u!zH>}d5nPwlMd@M#`(jhz0R zkaWV~UtIdHzyGJ}!hZks#YSG>xekR}_}%BxBS+NVgPxVlG2g7V6<1^$a6PGosoH;1>$t@C3yb`t zt!`X?tNG1s4px8pTI>9NFl$AZ0+xBImSF@J}k%nop=72{TzO8Ug)g0U(^Y;&k+TesI__5AQQC> zJh;KHfwO~`FPO;lv;I)3AJMCx*fKyq)EKrB%(lDr*SNHT=j8CGUbl+AMw#yy={byDIw! zCKe1RSaSGIW9}FmJSf;=u=Gtl*Q!kL^gHFXXa^>?^6_$7@QL66RljWEH^HWYJ%m?z z%jJJ6zv)&MpVF^Q~Yrv>*2o)G*exLtTLgG&WVdiBI3m4O`6sG7=i zBW73+_);*v;12N&SVgSA`z=L_{xLtq6Wl+TNwASK<36ekBRJjx+v;dB23Sb&t6MEn zRW2F4CSrsbf|&#-3KkT54`vl{xw`SF%9CR)*gK2`k6-Yj;3pAl{FcRTfrF2$A^zGIAn01;5U)GV0OW% zg9is64UQT4lAHMO}JNRtdO-NPOfbWPi&KEdw zFtEK#7gjm>!nKLGHtY&6Xu87fP6)+;2&xSY5?L7e{!(2*jupF z(3n8q_#N5^_zm}>9#Ussu1#L8G# zLGrkrs^@?;BQ}T$VvT&nIfIr%L`w_tNN=RF%Gf8w1?K>B#Cgf8M=F!Q)=UfTAG!)S zQ@fp4t7}EPF;Dad=KRgxSL$={pN8fE&H$dnI-!e!eTCKl>JG+3KX^WI#z}SG@6Z0G z?l0;9&Kg)_)HUdKVD8u(oTdHY6}8};q22Jwe1&qoMqVxAhIL{OkWZ*l-wW4N&on&N z;rR|G965`<1aprw0*)CT>4lD7SNlVKLY^WYP$$qAasgTl#=7NdeMfwt8G!SGzOWBB z%*ts|6H#wb+mO#_hxLPD$9Ifp_tIPo{S4$WcyqiXQ0tLr$R&({{Vg(It3@3`JMitu zF?hWrpV1z@cG90sDaR@z`1|Y;D;Jx>giFyp(5}Z%u1ojWL2J7yAEnR)jpl|GT8TTM{ zuh3_o*h*T=5phIrBd*XC!23S;FE;|Y5y*`|ZUk~8kQ;&A2;@c}Hv+j4$c;d51ac#g z8-f2DB9O}*{%>XubLXC$zqvV(n*+HykedVSYR$%bQmpQrbKz!fo?4yg(S&Pms>FMr ze6`xU-jOqFHs$*km3UR5RorZCWnS^N4WB5e;W(4hth2Y}!?XLczh^@(;lzCK-Z!g= zA?0{y+d+J_ZxJq1!p1^bg?rX0%2mwgai=ErcwYHtJTt8o+o!f?vlDap%`G>++tHd^`8465 z)>OI&8_+H(Ighg*H4QZaGhGI)ER< zYWP)tFP^{Jhtnd3vCItR8z0Maw`6Z_5-R?`)5r7LDghj|p&zq&B3B9?!1_OW@O7s- zoZd*`^+N`+rK2~uwVB1AN(Qo_(P(Zp!-2a_9m-~*W4UV?Yd+P_m3Mz$z@}xL+2dXe zZ)hiceC86~IKL@RoY$LItkd%Py~1@{&Ee@oG#t2e7^iQI<(gMJ^KBmuZ{FLVV+RLu zaPVCIu-S#L_YGjX23|boWe5-aq~-8cVO-lYly~(U#fwXMai7w*T-$sCUmZ6=3;;dY zx=vm;DjFu9(hE5@GMrx(`(c&e(VlHn+}Lx@OkQSU!-n8xT%ep6e>?8X86MksYtyOx zUfA88@pJgwKwo}Wx0K4dMogN^_l$jbOmZW>Wjverrv5LYvzj6d18$KE|j#G z-H(aSaKa4kF@F-rb{)=XKioKDVF>r`s_?q@)0vm|WP`g4cS@hiVVULlm5&EMoji}D z&r3`n`*Der8a4}Bz*RcCav`^E92e4&OT3-OLF*^;m6yJpHr9{p3>YYUayU0CKZUJb zMCaha7_J_?kln?L+B1C+`%db~d%Lb@qopBS*LDa8CAVPDgI-+mSO|Zs*Mk!RX7Yv_ zVZ2HS<&JNgvCk2Io*`b_ddoLf`Rf+sxSxK)tB>dXHGH|1YXiRgAz0;RzZ`MpagX|Q z+!K4Qov1MFXvDp@2Xd=&6S!1l7;hKPdE;f_{a8XY}_`6-7EumnX86N zFLC296I{7NhX6KR+=DAL9L`4v&*bEj691D^*!T|z?lsGmBU_E;VlEoKy*ZQ}Tsm>P zeqFfD&YqmF>jWN{K7xCVh~gVQ#G?xN^NGci*ka-WUX|IDzwL75l+$j!#-lrbkXjKG z(T|O02l1p$QM|Kww_qIPLn>q5buz~#b_u zcTYCm;LWFhF5?>w+Oh8ZT-H(_zOr>N?=I)c9SbhtXN7!t$Z-vORn_wTlJ5LsfIWMs zh42X5Kpxk4HlM36IuDWqK`3jJ`Zc? z%8u=(vQM6FY&l^vuM<6pB|*Ko(Xz5Ur1oMSU%Ur9FIvb(&qdeb)dc<~9Jo)xnHDqVmbpCj(L5g0w=<95;Lq+-H=j-@&yQRC z^M;}J{Om~!CbxxLv*U~fxZ0KB>={#;xI}dB_6%P+^XKWV3gmMn4~Xs=M+4db>cy* zaS=`x9BIA=msJm7cy@y=h3~U)s;XZtm~;3B3ty-gK3C;m!FzreR{YX84XOU$!UInl z;GqL6E6gmpt(&i(t<*7qXB6($5Z!cyL6}b+JeA>B3lC?4iD3}7-R_022HXsXJmgQ0}Cd9hiA32Oi=EU_>MpQ#V6b5iv^2Ups%d4S5^ zN<37i5NzTF*Cg>WP7&s?oa&j5wZku07*-4L*`Fn5;0T3p*1;QjmaymWL>AtWz$A_n z&T`SS2?lWX;A)2@I+AdR8sQ36zW1DC5rcT*!bcqpw{XZU^t%UrP`z})4}%W{cN!&L zyzs6Dt1N6H*k^E#dT{D>yZ01Uv9<-`E^MSGPCVn`caA>6R*QGM2Kyu)-x~2ywiKpV z_*Fl!yArdxmcrUwf;R?ZFMjC+|Kp<_{7KkK4fF*hR>G_H(10P8_6HCvcpt}$-!a(w zX~HFgg9leFOsMe6tqC6P;8Ml^TT@Ute#}95`2)hutK6?Jp9zwediZ<8i(2+UC)~Pl zxp@r2pz70?_A%U&wK@vJ42Byy2t5GkBZw}6%%w45{w~Jd#J@cN{@93zCA{w8Uw;0@ z6tLrJ>=55$qQ3zB0q7NgBNk>@)kxSZOelQXgjW2Tx}4^H$@J^Cas&0KaDVrpvj|z=s_1gx|Yp2^eBam(yU4@O(#p+In;oAMt@2 z#FZP}5XSgcTVY`Jl4mNfjJ?Dek{pxqYFo%Zl~ad?gmAVR;h~{1AYRIMR4-+);^I{< zOz%<)Fz6Ty>@&`|Ft(QPUYGn;_fE7R45DWsdI{_G^~CcW`SY{DO;uX}S`Xrhu18Kv zUzXsMWgQmalieh5r zY7)`jP~>?r=ucRjIpb}p{Gqi{p>>{n{jnpX2sZIZJ253V-=i<^gy}BN- zz^HFvosmyCJ0rv97`m4zX28CcuA$Miko;LExhG7v=pr13E&$2d(L+mOeygGcUvcCP z&Zh95jl{@+HRGI$PK2=b2cX}e=Bnt05Inl&th57v3|1aB8Xn_j&GHx|_M)>eTVuah zkux(HIir)>BOLlR@%=6*`8CMGwPZ;Hv@O87`wDlDvn%;-z?u;==vg3V5Fg~BcyNnW z!XRDmlO6;vU-C)=?|H;f>awuZ9q~`jybI2t=sI}l!0Cg_mUE@n8k{B6!5Eo`>^1Zt z=#`f#&b1DlH`)uIt(Vvueij(0GVD12(7u31iquif&}|zXkK)Z9PezvxRTF0oF(iT>Kgcb(KOKf zDXhE1Og}`%gr11#ROp)3)EWv)J%dNR?1`$c0zDMTAx)_o8dVPgx)i9T;?ZsZe}BEX zGvQ2wt;f9O{HyblJkVngr2bh#+oO?BF%9Z7VqI!=K7;6Z81f3oF3+};cNX*nG~+~5 zL(Zf|7T!4)Vbds`hK>Wa>g!?(o>U0YbsY#PJh`)FM?mw;=8q!gC$Q=`I+0P?CHEp z>6j;U-)Si&ix2QJb05k$SeEqP8Y)f37FOo_=TnXx`ax%vjTBSIlLE`$ql8xPs8}^W znqYU1PGzp3_B-P#aIg4wmx!k+6JzL%?Gti)no1|%=_!AFK4plrF!DZkDemqg`qX?b zg}FAOlCOW#hv~~{|GrB!>sWo|qwWJm4_!u7inm-NUGEJ zGv&!RK?iD>D0#=1R)(*-NZT9ypwf?bk@n+SG7|>T*(8HT-Abk>!W&i|vY$+kWe`_C zLGQXG(pw*6Wz^c|)KR>MyXuXVGL64b@P-&NT5GPj3=5@V&&pGiPMN~{UZHJ{>*-6U zGKyct^OP@73KbfZPJXThm4kgo6NSI0c~jrg^y2*}P`sdPE_zFyE8L*f?(gVkNHl$} zwv~omIz&dFC(`RxTd9vl5#`XtA+(~8R&n`wmhQ}7M|mDUr(;)-(Te(q$l_UN`g7l2 zs#|F}H6Fi^lD-=&bB>f!Chr$kHToq@O1(=)7gIEYG8fV`S5xJR<0Z1mY@r-j(T#3> z&Z`uz{gG_Gub_hCZqcAWuTy5^4l*hJl2YG3p?AX8wyIJ{T+d4?)0bSN>0X;DWy(rw zcKabEmCB&W9d^>T{ky1d{ApT$=pOl3i>5Chqv>jlH`RKvop%5Eo_fu`N`7g=t`15j zpZ+UozE=jde-le9$DShlv*H==S6u0}Y#f!%cbB9eLG1PTF

JQd^aTyeMKd9?;y8k>C|m~ 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%0Alm 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

0wpcxMdkz3?jL@^ZLtbJ}mg9*OPF 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%0Afwa6Mg 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�~e`9O`yy<|>zsYUO03O5i#kNPQis^S2DD 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

8Cx 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

!0$0J1< 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ȓ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

ko8PP^B_ID5{SFrDYt z8R+vzZS(}+J(oNWH=KsQz?aEQE8!Y+^<~(fm|6^%uy(G5vW)S-ySBk5A`0i&r0|VcDV#zf?iLBmymCk!~3SQF2P|RAf}k>+0a`>8?Za= zErEaI`s?7f`&pae{_n6if>#oM%VE!F;gfKvYc`xrUO5qbRy`D&7Z6Wy=2KZKU}6J& z|Ml1oj=~QPfwz<2{ta$h$aoHAeTJ2b$Wid-MO=dm9$;+HVE?P(vU^!WVDWRvIq+iq zWjTD5b?ZYgZ)0*O`@_B9tR2uFocT#4yb6va|C|6}P9MTI;K4K6sSocX z*3XA^563sz%YE*9tS|5y#&!x^fM2~AHu@av033!r&w!`Q!A~KbL=J+FpzmwQW!J*5 z$>;aLp>r8Oe0DE%55H!fzk$t}*Rx?c`TaHUMQr>{cmw^P0lWTfuvfY8TfD!4rRd{~ zpI{5vVGlU*GIA-nM{uoPNFK?sCvm?A+(T^M1>bbA}PCvCypr;r<`0T;iJn1$yrW<1a%zR%ru@P4KIH~SFyxA)*X@bKNd=YhQ@*yF%EPsE<^DdO{U z@Zt-J1(=C0yPFdm@XtHi>DK2nR(KP!_h#^2#vL%Zf;{<5>X- zJK+{$qYslikT>9^jPX@)8nJsnc6UA?mLb(%c-2kRH{km0K1J88=g%v6U&=lWK5`uP+mLk$=5NzTZBMK`P9x@D|U z@KWY;8q7s^Tf&vZ`d459_wNa>$0yE*cfOrHDV+8b@(>)$JzY5ca<0KNd}Ib}`77)J zC(IyL;X!op3-}T9n*`s(+y#4Jn+5RQ`MjTog8#k|R_@H)*k}A1?j9#Lh}GlZG4>NP z@ZTTJ<~-d0Bi6tr#3x*jU;Y=I`EAx&nEN*NVsQ079X^X8PTquXz#9^N42NOQQ{er~ z{~~aoI13i;z`H0|LcHw)+cBS=;bO+U>0I;&*RifQpzmxrgFOP=#~4<@E#Jo%V3Kv_ zJJ2U4eg~)EuP4C?2jlNEd3FZh{mp{QZ)Tl`dk#l0a5_Hy8o20wVheuDwL4$|_h0;U z`~q%ZuGhg5>MezR&f>WU#;;_bi0^&!Y2-$jxi-)5%ZPP&%QKO%9eHXVT!|jO4tt=} zWsr7Z?+eR2*c)ECE`9-DWlWcY&tJcSO~}trhYOj%?>}eK*5>ezr?78>6>lUC;1={d z2|JAOt_zOEk57i3$fp@j$4`Gvepv-u9g7X%ybt2%upaR@3tsjO@+iD=9qxtuY45i% z6WyoCKu7hZ?XUJti4^aZEi#r<&KYq7`c zI6eY>z@@)qpMbu;2H!xR--ZcT40D;|mheTMFIOIkeIYU5N3q}X@3F^(Te00OaKmlv z?ZNkT%VCl+dd8mWsQJyY4aU$#anoHBKX3!aN&#b9k`2paT6SY-S>mr z4#0ZFKh$83%UqS!QZ}n^C13TjgP$v9>CVC;Ng$c zKTN)w`NP`e%2o9L0Ib3frxOEt8Fq)gcP2-`%09X|8{NQ6^gIjhrQRyo6Fp}*@Xh3N zIA$~K2Z#Lvo5Q02;2cbJi2HT%eVFxo^Z?Vz<1=8{AJ|v1_iDq=)ZGzQ{E&C>#Oe>> za5xO6KS+I8j{Z)9Z){48L2C{^053!*%MYc0*mN23u?_Km3ikan_u)TBU4t*eBM-IH zH@zQjWY2a3yl?ZtzI1cuazFcwE#E;b!=~7_1sjgBFNNK=!nSY`I_bfk=>1;!0sDlz zVZ(p0zWf~>UdrA9_P-VTAI`oW)_DfcUEt@de_Mzx;0APg9lVGbI|`O8z&F|7b&;1M zPhn24fZvj@{|Kwj#rNT0a?esY8=I|wpKnc`g)RTWxZz9e113+z#_-sm854Z>F7`*6 zw&9+m7&pv623x}s>~Ri-3qFfJprG&DneVOe5c5c!Q@7tD-fx137(;q*J3TF77dY<) z>>r_Z0x@pK2)6YKMr;Dqy7i(zxdvHjSsBxfi*s`#s}8;z#1P|;{$7aV2ux~@qsly zu*L`0_`n(;SmOh0d|-_atnq<0KCs3I*7(30ANc><2mZ1+#ou2(jlbpfa(>s~5dK!z zq5LhdO_1R4)Z~4ar1(3-8&HP4?ErtLCVqcocj`Z#I<%43LGD7m-H;sTF_;ZoP-i+4 z{2iHWowbqR-z6@Ksn0d>ZwKdtIJXUQYtHSD>>zjKp7|Va$~F6h{8S_if0xSNB8lI? z*dDnZ_w0ZirygyTWx)LWTg8Qb6XPq)&pF0#|NaqO*bl_tOjdHQ_s8#c%;nky$A=Bh zHJ<11kNdaCy~p+&uK9P59b<$38jhu)L-FrMt7ml;I$FRvb+b9;y&-eF2kcqv9m(-A z95Y6L6C^!nfPWV`epByxoCkj=D9z+}Gvu=X9mn6cem3Wzw19j48_52BWOdtYMR_ld z>90XAg>k0CIi}C7Jz+Rje={ekZ^ye6ynyoaspCADPuZUP)i>?=`$?fk^{oy!q7L}? zn;mz<`LsXB{yvO2SccP!>mhw*tX#&A6C=o|V4sN3)Ze8RuKtv?m}J%Hmy zfIs=0C-K`S=px2j^{&6=HrL0HQ2vK;pnuCnU4g$7k{D;!rk0t1nGejp!QUF zo$oB5k3>xA3o#e#Fn{xaf8Ra)*0D2>p)VwD1O7%%W*+)y!x;Q6m&|>UCy1ShALdk? z7xqoZa;)8!4(g~2>)T)W%J6UcGcSKv$iF*Z=*Pc}@88IeIn3dj`d5i>Xq&JT z^%DJO#u$GKzD=3jXa3Xo{k!+ZeL4~jr;NV+d*#{rFc<&cyW?{F5jR_L4PVI2ONM{1 zJ{jK&X~Vfg!#w>>GJQt;JMro*Fdm&d_RuB?JvEG_(WmG)8LQMy^sg`K-;OV15A%4; z&GDFbypK7Uqm07_BA!m8o_^vv{7kNE2K?1oUbX5CVfTjbw2oyzm4Ya7lnR*$Jl;T*Dv&*7@KkDo*;e)!u-No z)2LJRrSI4dn5POp+ziJ+pN-?zM%XWDzqJRLgUwY5duQio?qJPuZ7Gyp0}}nHbALNR z9mQDCedJ4Q(wIk(VH1#Nj#`~CuHZE0nd3}xrJ(%;}hV%_cAe{KRE89zoKu)7x~U}!n_0aW$}9@8&Ds;W$m+o zW5$vp?aRHHW9I98JlA;MF*$GQ7VGMtj3xR*zYX*9x6F*Gh;`>l*}YY>eXVPK;zZ(i zr?6!)mKGyV0OyAuW6e^R;(TW!>u<0`J|3eUb1b&I$$;={a#zfoxi`*F@KNJ3;|uzq zzYp4Qe_j_H%cd~SF?x;lFdu@w6+^#R%-%LJJF z949(3mqnTS>U;P*cG`aUyCvqoB5P9zJ?g`eFX$^jjWU>H6SlEkV=?v_=&j%@#&pBD zSLYhW**I2zcgDCM{uUr@XX?676!#Y9cH57B@T-Vr>v6qQn>$}^l5FcK9II3ERipnw z!#L70NcfCskgIt?>Yay#`V%bDPUG3)XyopL`h_{W_cs?5Tj|qe=iz+#XOl;a&E)e z8RIf z>APS{_a5#U+;7AjXloeL4SpN-p2{`BZmucrYs|;$Ojh@$jmag2`4#3Q=H#eLeCSX9 zmZR@B+{?uN+`S>|gf?h|_OflRLtW!`U&`pXICsv4kD}9qzE zwFV-#=sV&YU-nwem-vhr^?BE4CT+V7_t^#*XWHNWtaH*Y3uW`I>x%1}`J><)v4*J& z^BVTe=sRMLe$5NTv0xkLU!3C-&bK)}fOAl^7j|<`R_u$s(yYt1<0&75WZtgL-V^t+ zZiR0%j!gUdN90rIODO{1;22An$+V` zV66H}BiOakZ?XN*=CNksx7I1>B=$e}ihHMsdHQmG_9^OL|B7en{V9vS<=pApe&ToT z$X~flJM?evi~Xtdnob#AdZNLF42!M7PjX8$iwOj{U`LNE+W=!1HCr* zROB0D)^SY0Zk!j#s-JAYIqnOzr}wUpJd$g+H%7go?bSEejjcpXNk(6>KV+=sxg0;6 zI`~}i9>$v20_+v*40Dfo=NQjT^qbIOr;N_jTOy7f zv-u}{a(9l+2aL5D`Xn;!I?nlhX~X=(INX<*YqW37g*}nBY{mv{#CSL6I&CNQ1@37J zeGGN&`NaHv$CL1%7>i@CHqFQakypqqY2g6R`3@|LbJsrl3dFMrbItft=zux;8<_e< zqi=W(^rg^?^Mjb1bD)2HKz%mq6hAXA^vmdjG5DM7@q3x%v&Q-1Ke2Y|%h;sBUvnGg zaE*P3*Nd1#yx;M1uk(t1h5hQg=tCX2Pl@x4MIVVVah#@6PEhruE?rym3%NGT#gtuZ z?YrQ!!~7Y!4WD*>(!qfXS@0g2D6JD)w$k(9xN=(zXiu0b2^Cf1jpzmq5I5OqYcL3GotUVd`#hk(!o87W@wM1X zsjFkSZ>aC%ImRaLxAiyo_Rcf*R3o2ckdH%`YUwKxp+6X zIrq{=tOw{O{L}l1&6KG_Uy=W{A7p&U_0@4E$7}!F0{28-B2L_|L@cRyuTA3~&N-*F zgmd$d=rG2FuQ$#a+!M!VG5FqUJu;u6Z^rKh`&k`xjXCelJ&d6+#>j`{F!ND1F2Q`# zSXUo3?$EKiHb#nb!^UYg$NBZXLck^+K+xqys>zckB&p5C-uu% zyN~3aBdFv3#D#e`;nU*UWP1f-Z9#7h^EQvJ{=NFmIESt?ai$)UdQa&1rgllQp9Mrq|(8sWGLdUTdZp}ID-C*a?pYgH@=SE`1{NPv|cXJ>xUgwq9;~v2_ z4K`9owr$Ml>lu>ovVEq`lpPcCmC#>Or`m=()(u}nH_lrfg&!_PqR-Ir30$Lo_;}T~ zu@cWP%r9aEeHi1etJ!g^%kfU|eA;1Nkz0&)Y!Pb<*tt$1z6dyns5~Ux+LHPkY;6%mp9OSKNO!pbr|K(Vz9vrSTIs zA*Nz2XS`{Qdh{9VB6-c<9(O;bKPSc*{n!`xJ092RSZ}#kJ%_ILqwQsY{}$%kqzdD6 zjMmfVBHkEd1H<1tHzp%TaZlI?e~b9}4P)@#Il7eOv#<8`z5FVqag9A1_1gmGP@rLM z>ech1W8%ix8ID!o_Wf9_@xBM1L%nUdUj3#)_qJnRQIB5B%sXr`96R=No)L@G_a5_o zL4UC>+(r9$Fvg#QzXeZT(cc||e&srz^=)mm2a^2fcQa5VULJe*L(U(_J<#sAN1vdeq4XzT_){E zefuC@%~?r*qE6nQV+h^MM`{!GSnnO`&2_dh25bY{nFGuT=FRZKd6c1yaX$=wA+G-( zA7p$DK5k#?R0{hC>&1IQeF_~F^@L4gf1tm*mQzPv=tJhw$V=EZ;WO^D0?xap=(nO> zVlRh|3mAv$T>m!?5_-$#y)mv~$Cw+oS1$>@G}E~59LZauf94SOP&dZ3{;Th)FXAov z{A66M&w22@Oz2pZjDz2Cgw)w{Gm`YJa}($zWd*JND=OLYo0TZrluY?;P|; z{lPi;E+zbu{+yTi9Ift?Hsjui%{mYLUH{h?&CBLH_p;`g$c_6T_ofc*y3S?h6mdoF zh-Y2Kn(@b&8-2Q$ikyedqb>CT346QN7f8%Ebgccf{dSbW?_NcYXDs@2Koy|=5fdDoVAbOALh=y7`YIM{v#&!9elq*&)QLYXYNgc zPFJ@>J&$9(A?KxEL!FO4V|+T^c)nsD?#Uh7YCSQ(f}fki%p=BzYj*l2kT>G~c}Qp+ zCw`fURqYwMfb}Y2M?WudpW<=CR*`F_ab7NQp{SUd1HKvioL?~JIkj)@)jzZT z#n|JW3+*=_?f@Yd-{Byh8&{|u4e@@Hff;fKj<~q zL)Q%2N#Jv_`*Ho;++a+{nqmwII*6EeOzKV_U7ut1@4FrB6LG7JgtpBOS>55cIRkT5 zk4e8$SL<@T0CuB{ANy{`@fCcbVcy|$&PDLU$ob}>>0H~DI=gU8Z2Dd}^2qL#^&9eYwHkBA%4w$p*~h8J|tWx;D_qGekVu<~o$ekmx4ttj|LDu<<~CjT}oI z{V(!`^P0;sxb{YT&Zo?GS>rpc+5}urefBmt>c6p;^0{v0gckRWanAm+t3=Kv@A{15 z{-QaSI>#ZgmFrV{2H@wC*sDR;e)m$ccGZV@hh%P-$RqCG&3ijjAB+R>Wt>7Zw+ZOr_ zuwSgFw3YUw4s*%+x&FK@$Jp9+N56_P=L&JwKW_`iOUW9nRq=`pVFT`moQc@w|%e)P3Om=-+X~n6-y-h7B|0cILEO zI0q36__{g@oU3Si{5sZRY%=tHW6zjyUzIsdvyn$4q0He}{j>hZdkONUb6@=q#P5*0 z_AI6zIxrWzt|a?5_uwZHEBHl>nR27=%%^-OZL)`n&yo+Jp7v*)t{aK@`R?95QmonN zPFoo#S^oxek8L({kiOAEy;ETfMZaXe5&Mn{d|oKLr^wjNcPWLw%w4t{KD&_m=uaPL^aFfg=wFdD(U+fP z_*u+Q7oJy_=1=!M<{fi>tY_qeG>2pS)coNyRqU_vx5$%>A;zLjpi!U3ss7;DjRkzs z^>4M`7#sQs`8#q1YkkH~CG>&4l*T}&PUJZI9K zxVQ&!-O&aSd()8ES<37I=1pTUYYQ+3W@66$dH8{2Ll^EBrCG@JOqo4 z;xTM@D(4QO9{xV`uV~wSK6PVHY2L)viM}0!V|72``WbUm*XoNnE!uN^Bx{N9qa1JK zaQD}Y&vh)mcXKq7zRe}(=;SqI*fJr7IqDC|yn-GRME_fHTy53Nzc62@9 zdc+sU%^dD0uluf3UrU1SY%lgXj^A8tPUgHhY;{bz&-A?SqQL!E*`Is$f5zzZo_-#_ zUhUv|q23$t`Na2wt99)h@zt>HKD0%gWqirFG3V>UnRqVVyO86Z;02sFpP=JdLpmJG zaE|6o*BRy$?{yY)%{ZrR*OF!lEajZf>psId9_NA$#ko7jtUW!){oZ4KGhT{lZ|kB% z*KMDf!j~N1Lh2z4Qoqv|^^vTfOdH@@80#U=Y*`&Te*C|H>%KYH=UVp=jrwdutz(?H zCM5F={@_|)wX3L~Ol)dgLPWXrjGg{X}jt#}xc2Z;xax z&}SWw{@)k__Dh^+))@UKq2HJT_6z-F^#$s|J(2Ai7uv;fC*QNshW0i_ zVqc{$%tz)d))jMlVH1Bv+p<0#Cs9q$$Sy*lOx;@M}Baw z_)^iIH>N+I>lShh30t|YxkePnYK)lss$WDtR^PjE&3JR&rX6ij93!|#bpNG|3UO+z zI%adM`6TpenY#9)O^dMhx`&DVtGXBbBJzhiKY+T-*LBmmn@1dLk*XJ;B^t(>s?X-C z*Gyl=Si~7?nE4>~Z}?2axUr1=;@t;!Nxr8tCmg~#VzHP%v@i8shZ8m>xeW4}wg;aB zd`H@7Q~GrcH2;Pz6H+^~M;zt=*SBnL9oiCKk9>;E9|9< z=4A*wVK4XJvG3KE`ptS=gJBM~eRb>l755UG&a-iB3pr0)1%Hb55_`KIWPQ&$;%mdX zn}^dPuBrEZ!7+(`Lfppw%yk?+8@Hh&`qefCzX{)|x=wY!7dl0+=7J3F!S(IfdthVp zO4xD^ZTS49Z=jRNZIq3#*pv9~$9C}fy=E(uabABB`ZNE>7>GUnJl^S#?AgNB=4^ZvuDa z^Dx{OM4U6m*k7nOa(9f0yd3Xxu)n#34cUCS$%2ky+kV78f zSkfZyt89*x1(hX8Szb8s)@&jZa9;`fsd>9_9 zOvk@QS(~z~QyD|b29>8GM|m1$nK^}LQI^drng$yaDBW>O=a# zfTFwrJ~+aK99Ifb(uXS_K|=Xh<>N^C#1t-~ET0;XF6BPCY(V(}=PT)poO?1>a{kG< zg7YI3jz>vXa(yaa<(ynIpdclEZ9qXvx^_T8O8Q3Sn@DK}l03{vP!4WDetST= zkvj6-%J-2_hWvnI`Qdm~Mgu6rYx^8@q$2(E|HR9vehvo1?h@%bg5VO%%e zU%7@tGX?h%Lz4T65gOJl@jXYp>p(W(KH2Ascy?jkD>524_u=BZFY(=O zyx&DOBgFpIJ!~Z-rMM@7GIc0pFS(@6-II25bQ)9 zVP7fk9ah60BY}HLao-fr2T1py0ed$0S*7ALgOrNT!BQ#>dzvA!_gutvaGx>6eTSqa zPr_2pjo@=;JZrW&UnxjQodFpsWrX;wW`E9sd-vh~-G84VzW2p1`#iJ`w+oQ}7}r zG()=7JE`&#B;=C^G|1+q1M9heEIZApZ=U??Wg=0y} z2Q)}2udbYqg#4NT>9y1wA#>aec^!3LKcM+n&OBhO(shK9Lq>=~fC-(ux83eT*Brm4cMs2o1;5J8wXP9MXC}?L)5g zkxxeVeENWXC4Yc>pkHZSK--n{LBr>nZ-Ae0X%8)<(L<(hT zT{0jeC4FW<>r&2Fx}T*z=vTU*qYS0eLypo=ZazPtkCb%TfP$2MrS%2csuZNOzBr(Z zd@_2Rmu3om%F_JjfYwUdfo`QBrS&DQSGt#T9rBk46yy`Qf-U`tQd(CJ$Vll`TGwzMN~MP!rTH3tLAv&Fx*U(vqb#kj59lMMe1m%{ z>6_dS-AaR$ZZn`k_ABW+>OxcLTfY8@G?a(*IhORT$LVq`rP3htw;!j^v2<^Iob+GZ z54qAHCEYZjAf@@v6X{cy-glo!pR#nn_e2`XL(YFIp+GZ*K4t0MI-rk~?rj4KGWanP((MBp`^gOq$HveHLNx{L8u zGE&O_gC5sM$v@>j=vDg2A+5VpMZddmO*DoLes zBspDVcRNza>8g^fZl6zgr;Ai#Hq($~z6XtOXjnBNx0!<`X~LE!AsjKpgpV{Pgd>e9 z8n(oQu%jU~{GPA1*EXl~Fu&iQzkl+0JRk4RWqsCqulH@O&pz2Zk407lGROi6$D%4w zR39CoG~ke;0xBWZfa5?0DH6y9?4iU1RQwkzxh|4G7FnIrhKvmsDgp^oq#EV1NRfu} zfJ0UWN*mJ$*gzRlR0T>8<389x8B%1RGGL1E7Ze37Qlx88!MVr;s*s}e@H`Hg2~;6P z7D}7sDMx0YGEj5`V}cEoA=7~jQY3*KvO1Zj+=pDC=t%miMk&YDNjMkTK&lz`3^=4P zN98F)iX>2lEGnjbAcGV|&GVEZg@sg$Jf+A2mSd3)lp}=;WRRlrzfi^XpCZTgpCZ)~ z9l%0mpbA;kDo+|&opR2F3sfRS0@*<6(aZXI!_8IT%ZD(sX^s2 zJO@=jK~ajatC8hcUFR8Yn|n1`?#m1&TW2<3I{2N&^-tGJyms zsvs9I$Ds?@8kBJ^Os70$$Z~Lj3Z$qEB*>!9|Dl}YY#@gesV;faNEfI;iq!Gw9VkPl z0~w@90;VhaKzSg86qS$!a>&#P`%}ufNCzsAA{QtvbqP=ypZ z(LGP92hTuRAdM8|HK^b`7ce~;3v8eaDar#CNKqLuy=WgOMG70pAVm@|z41XHgG>Td zNRfqHz??|G)ktw%oiffvdB7nn0vV*p)xh>aH%R{%D!DGI0_A<_A6y`V6uCgEA3A{z zWRR7CDx}B->`AnP@_<8%szA0s^M{I)^JI{sDv(1K4cMPj&PCaOp`7cY63i+4Q_8t; zfhwdZvw70UnmDeBia-Wgohr`D2j;0lW&=5-sJA>aySv0@<%rlk05={RnJg5)6`3!# zM&`+3v8h)Yn_95(sMx#+xAlw7B3RWTHisV*n_{@TJLk~zpxBf@6Pa^hN>yYg!R%i~ zW+`0%O=K28(c_V+4Kvn7W+u!Z7nuy?PLIrH_)Sr4X2b0##AYd+`(R{dLHXyAnFnQE zBeQmTWLCnq<74v=_;ttF%z@pVW3%Uq$UJjxWR}BaN5|&Eak2RXaxn3qk(mIy(D?&s zH6S(}V8V>Zl)}ZsWAk%ZF)uO+G`u-7-&RCs^-G+Cxrk@WBhxmCOdr@cF*g5#KFwm& z4Mv?1@o#z}bLF>@xe>mik3GHX%ClA zj?GojlW~f%aZ705oO#2b4OhR+xeRt><TGk3v6r}o9EyX`kf6^{uY@>C&p$qv>qC>ZNp}bBC}~rWU63P8rwtm{m6U> zGe#|Q8ia%dx* z-7+@Q;GEx()9@?&@Jq;It6k7>3O;~uj*U$n=K0bGk+~N?FM$j(w+z}|PdhlQBsR@J zc9Z=c#m<#rv0rzX#rWsKHQ4vpaP%SM0Q4swj-n$5VmwD#$nY&a^mmU9lB5- zTEmAG)Iiw692+vfMo?!weM99t*bi>mK@P!|U6FYUmZJCVu$gP`z_hiIxo1pl)<7%9 z*l-$kd^vUNkCDlCjm<9d!;t5tS4XA(48soH zVJ?2351l*3rWB@=$K#*^|6c>s@b}qp{e0pGW@DpEU|=_Ng=rUId#LjQH3VKeGd63f zvCqJZ2cZj0C+5zEf#@~_c2m=)e}jKU#OAwZ*am)y&o6}RUTO$jz;Vf4w#gJJ}&46{Z`77+Yn0UE_wRa(F zFLe2ux&W1}BJ(Bf_d<%e*i9bqg(~X78}P*xYAt$d%~<_iu>f!WgII=!*s%e$!3Rxlqdr2_!;#qz*&(rc9US?4 z1vqSUDO`?TSHUd<*vCNoNvw^7m^UOYGB3bB;_=_m=gY|SgF|TlEMvS$Kiz*zp1^ng ziMNwu{#h{XpglG0Xy`(Wc7&zGm8=!Zpe;7*16{8npI~@L^n%05&!#X0JG=>hhx^VTH;C_YsASE01ZGvS z*MWEN>p!7UDbK;llc>*7AL_w}==C{#OU~_8-n|!@iLi+pwiEVt$L_G^9`u5pFH^tA zQL~{w`P&qxvxc7oquP-lurbVf(H034b__wGYy>*#|*4)`KB% z9ll!%E8Zfuz+#)eP{LYU3ZKxX0db$3!JZW6vu<=}ztaZ>)?$r;z1Z>l8`w`l1{*Ge ziN~`yfb5Bp>7ODNU=8ctIv92eYc5n$10R8+&)A2+v6lT7bm$$MuJEs+)L;0PT>1ev z;lJ0QinVGJ99crnfuZhy*MK@Omw15gU!gmkOMFg)jU%a5#LTo(Yy#Pj*@MDK*t8GK zf0DMaqlmTeCTs@XmoY~e_8j>Mr}km501p!5t6=fdtU<705wQ%mGj@Xta_Ksl_bIx- zW^!#AyhPr=249b7KJdkd_=&aJe89T2gIu@}`@`xCdl%wnCG5bDpTTxW@WYeP{yWwy z=yObDdcw9xsjo1THdn%>gJb?_DfVN`o^W$*{0ncQOBP;kN{+)v#CE%}`F(1C_D54- zHU8VQjx`k);@6v@@>=v`zF)%@Z1o;o!8~WfXLaxqoU@9x3Ud3Xg)lo8nOgX@9;AE5 z<_yRki+!Q(d(0c|M3?2TkJ{gWIX_11Jp&7NN2UY%wTDBoQ$5%TAHmGQ#5;^z&v@`j zF?NLa4r2|5mgHp<(B8K_j2+GU4E#Xd_#Vm|vHyYG>AVNQM;z~h3Tpn%a3kx=^-#5w z`Ufj+CZ6GnCb793+)>=2v( zr7m3w|G@U!VA^@qA6Ruh<3MU4H4xTl-_V6Pg7rtD52W$c*|6~s*a;S6?_1z}YRN44 zEBUex(u{Kk9CQkK1s{@cdtmJY?D0T;lD`an)P}9(O}!%O6u#PgIlhN|dwAD_dgRu3 z^!4_L*ldS1Hai2hv#!4lc3*;%( zVonXA^mO`%MI7G-PZEo3pw$r8K)94~egzdzVP7cOOU;DLS-h`9Y9H$>tm;P1gJJBU zrqIuMFmoJn1e=JbEX-_%t}y6Z-p%1Xe4d4Ums6*hf0HczLGEpIg#PDXS16{291gFr zZoLVMUP0%P^Z`Bb_ZZl;*+diY9`~f>ncqlTzgdWt3KCl)$oOuTOV8&h#eeV04D#sPpSDLPv;R#0T&*^g0LT9zs6Bcx<-lXx4ie!Eu}Z z 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%0Dhwf|y;Ds@%1B`x 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 ZM;Vtv3i+QWyZGCvqG9@{}* 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%0D1h_7Ho}ioK%VGv;5+ zy9(?X&7T86%O%7h9PW<&=qn$(GS8kc|4ZI&-eSKE^|>B_hw$S*G;#!{?qhtgo!EF2 z&P68^VD=zlofv6A9=Zj;EQI%SStnp^4bG>cSy9aZFrG)`B&lhUi8X>`+KfD@@fCoM-C6FvG&&IUtjpEfoFa6EB?FOAOBzUj^%xg z34fj;V}pzaG8)KeAfthd1~MARXdt73j0Q3q$Y>y=fs6(+8pvoMqk)VDG8)KeAfthd z1~MARXdt73j0Q3q$Y>y=fs6+JXEl&z^nd(KR`dssNGOqJoeE(P;SUx0^2adJuf7aO# z<$)rnpOiKH&K--R9Y>By{YA-Cyn3k$|7KY=4}MnLO|xBbx7-Xw^KB@9Gpg{iGi8hYu|1IJ~e+myVq~4jM6VWZ|IE7Zi^i zGkEOaks~^eEd~1z z@EEjv#gp2Du*7M2de7$zuC?I`Nw`J z4@8QS_ViqQ|5zT#JttW|<^TWlhRaOw7zFbKTyx>{t1qTZ z?ccprg@3cGngY{{QOre_vOPtbXc6PCKfrQ>vR&_>U;<|9)6sjrJymRaIZ}|5usz{{fD= Bm0bV; diff --git a/tests/data/loader/flash/FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5 b/tests/data/loader/flash/FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1524e5ad375111035be9ed6f1dbd2ce48b68da0f GIT binary patch literal 478032 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 pwCO@ZSE}^vX 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∋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�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�(RjyICeFIdD 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 VrT#08fzlW#je*h_`2Twh{4ei-0#E<| literal 0 HcmV?d00001 diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 3a8289f0..0d8d1c93 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -88,16 +88,19 @@ dataframe: format: per_electron group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" slice: 1 + dtype: uint16 dldPosY: format: per_electron group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" slice: 0 + dtype: uint16 dldTimeSteps: format: per_electron group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" slice: 3 + dtype: uint32 # The auxillary channel has a special structure where the group further contains # a multidim structure so further aliases are defined below diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index c8417d8e..81f93021 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -1,25 +1,19 @@ """Tests for DataFrameCreator functionality""" -import os -from importlib.util import find_spec - +from pandas import Index import h5py import pytest -from pandas import Index +from pandas import Index, MultiIndex, DataFrame +import numpy as np from sed.loader.fel import DataFrameCreator +from sed.loader.fel.utils import get_channels -package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") -H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" -H5_FILE = h5py.File(os.path.join(package_dir, "../tests/data/loader/flash/", H5_PATH), "r") - - -def test_get_index_dataset_key(config_dataframe): +def test_get_index_dataset_key(config_dataframe, h5_file): """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" - df = DataFrameCreator(config, H5_FILE) + df = DataFrameCreator(config, h5_file) index_key, dataset_key = df.get_index_dataset_key(channel) group_name = config["channels"][channel]["group_name"] assert index_key == group_name + "index" @@ -34,8 +28,7 @@ def test_get_index_dataset_key(config_dataframe): def test_get_dataset_array(config_dataframe, h5_file): """Test the creation of a h5py dataset for a given channel.""" - df = DataFrameCreator(config_dataframe, H5_FILE) - df.h5_file = h5_file + df = DataFrameCreator(config_dataframe, h5_file) channel = "dldPosX" train_id, dset = df.get_dataset_array(channel) @@ -45,25 +38,23 @@ def test_get_dataset_array(config_dataframe, h5_file): assert train_id.name == "trainId" assert train_id.shape[0] == dset.shape[0] assert dset.shape[1] == 5 - assert dset.shape[2] == 2048 + assert dset.shape[2] == 321 train_id, dset = df.get_dataset_array(channel, slice_=True) assert train_id.shape[0] == dset.shape[0] - assert dset.shape[1] == 2048 + assert dset.shape[1] == 321 - # The data in file is not representative of the actual data. TODO: fix this - # channel = "gmdTunnel" - # train_id, dset = df.get_dataset_array(channel, True) - # assert train_id.shape[0] == dset.shape[0] - # assert dset.shape[1] == 1 + channel = "gmdTunnel" + train_id, dset = df.get_dataset_array(channel, True) + assert train_id.shape[0] == dset.shape[0] + assert dset.shape[1] == 500 def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): """Test the method when given an empty dataset.""" channel = "gmdTunnel" - df = DataFrameCreator(config_dataframe, H5_FILE) - df.h5_file = h5_file + df = DataFrameCreator(config_dataframe, h5_file) train_id, dset = df.get_dataset_array(channel) channel_index_key = config_dataframe["channels"][channel]["group_name"] + "index" @@ -80,7 +71,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): shape=(train_id.shape[0], 0), ) - df = DataFrameCreator(config_dataframe, H5_FILE) + df = DataFrameCreator(config_dataframe, h5_file) df.h5_file = h5_file_copy train_id, dset_empty = df.get_dataset_array(channel) @@ -89,191 +80,185 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): assert dset_empty.shape[1] == 0 -# def test_create_multi_index_per_electron(pulse_id_array, config_dataframe): -# train_id, np_array = pulse_id_array -# mi = MultiIndexCreator() -# mi.create_multi_index_per_electron(train_id, np_array, 5) +def test_pulse_index(config_dataframe, h5_file): + """Test the creation of the pulse index for electron resolved data""" -# # Check if the index_per_electron is a MultiIndex and has the correct levels -# assert isinstance(mi.index_per_electron, MultiIndex) -# assert set(mi.index_per_electron.names) == {"trainId", "pulseId", "electronId"} + df = DataFrameCreator(config_dataframe, h5_file) + pulse_index, pulse_array = df.get_dataset_array("pulseId", slice_=True) + index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) + # Check if the index_per_electron is a MultiIndex and has the correct levels + assert isinstance(index, MultiIndex) + assert set(index.names) == {"trainId", "pulseId", "electronId"} -# # Check if the index_per_electron has the correct number of elements -# array_without_nan = np_array[~np.isnan(np_array)] -# assert len(mi.index_per_electron) == array_without_nan.size + # Check if the pulse_index has the correct number of elements + # This should be the pulses without nan values + pulse_rav = pulse_array.ravel() + pulse_no_nan = pulse_rav[~np.isnan(pulse_rav)] + assert len(index) == len(pulse_no_nan) -# assert np.all(mi.index_per_electron.get_level_values("trainId").unique() == train_id) -# assert np.all( -# mi.index_per_electron.get_level_values("pulseId").values -# == array_without_nan - config_dataframe["ubid_offset"], -# ) + # Check if all pulseIds are correctly mapped to the index + assert np.all( + index.get_level_values("pulseId").values + == (pulse_no_nan - config_dataframe["ubid_offset"])[indexer], + ) + + assert np.all( + index.get_level_values("electronId").values[:5] == [0, 1, 0, 1, 0], + ) -# assert np.all( -# mi.index_per_electron.get_level_values("electronId").values[:5] == [0, 1, 0, 1, 2], -# ) + assert np.all( + index.get_level_values("electronId").values[-5:] == [1, 0, 1, 0, 1], + ) -# assert np.all( -# mi.index_per_electron.get_level_values("electronId").values[-5:] == [0, 1, 0, 1, 0], -# ) + # check if all indexes are unique and monotonic increasing + assert index.is_unique + assert index.is_monotonic_increasing -# # check if all indexes are unique -# assert len(mi.index_per_electron) == len(mi.index_per_electron.unique()) +def test_df_electron(config_dataframe, h5_file): + """Test the creation of a pandas DataFrame for a channel of type [per electron].""" + df = DataFrameCreator(config_dataframe, h5_file) -# def test_df_electron(config_dataframe, h5_file, multiindex_electron): -# """ -# Test the creation of a pandas DataFrame for a channel of type [per electron]. -# """ -# df = DataFrameCreator(config_dataframe, H5_FILE) -# df.h5_file = h5_file + result_df = df.df_electron -# result_df = df.df_electron + # check index levels + assert set(result_df.index.names) == {"trainId", "pulseId", "electronId"} -# # Check that the values are dropped for pulseId index below 0 (ubid_offset) -# # this data has no nan so size should only decrease with the dropped values -# print(np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448])) -# assert False -# assert np.all(result_df.values[:7] != [720, 718, 509, 510, 449, 448]) -# assert np.all(result_df.index.get_level_values("pulseId") >= 0) -# assert isinstance(result_df, DataFrame) + # check that there are no nan values in the dataframe + assert ~result_df.isnull().values.any() -# # check if the dataframe shape is correct after dropping -# filtered_index = [item for item in result_df.index if item[1] >= 0] -# assert result_df.shape[0] == len(filtered_index) + # Check that the values are dropped for pulseId index below 0 (ubid_offset) + print(np.all(result_df.values[:5] != np.array([[556., 731., 42888.], + [549., 737., 42881.], + [671., 577., 39181.], + [671., 579., 39196.], + [714., 859., 37530.]]))) + assert np.all(result_df.index.get_level_values("pulseId") >= 0) + assert isinstance(result_df, DataFrame) + + assert result_df.index.is_unique + + # check that dataframe contains all subchannels + assert np.all( + set(result_df.columns) + == set(get_channels(config_dataframe["channels"], ["per_electron"])), + ) -# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 +def test_create_dataframe_per_pulse(config_dataframe, h5_file): + """Test the creation of a pandas DataFrame for a channel of type [per pulse].""" + df = DataFrameCreator(config_dataframe, h5_file) + result_df = df.df_pulse + # Check that the result_df is a DataFrame and has the correct shape + assert isinstance(result_df, DataFrame) -# def test_create_dataframe_per_pulse(config_dataframe, h5_file): -# """ -# Test the creation of a pandas DataFrame for a channel of type [per pulse]. -# """ -# df = DataFrameCreator(config_dataframe, H5_FILE) -# train_id, np_array = df.create_numpy_array_per_channel(h5_file, "pulserSignAdc") -# result_df = df.create_dataframe_per_pulse(np_array, train_id, "pulserSignAdc") - -# # Check that the result_df is a DataFrame and has the correct shape -# assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == np_array.shape[0] * np_array.shape[1] - -# train_id, np_array = df.create_numpy_array_per_channel(h5_file, "dldAux") -# result_df = df.create_dataframe_per_pulse(np_array, train_id, "dldAux") - -# # Check if the subchannels are correctly sliced into the dataframe -# assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == len(train_id) -# channel = "sampleBias" -# assert np.all(result_df[channel].values == np_array[:, 0]) - -# # check that dataframe contains all subchannels -# assert np.all( -# set(result_df.columns) -# == set(config_dataframe["channels"]["dldAux"]["dldAuxChannels"].keys()), -# ) - -# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 + _, data = df.get_dataset_array("gmdTunnel", slice_=True) + assert result_df.shape[0] == data.shape[0] * data.shape[1] + + # check index levels + assert set(result_df.index.names) == {"trainId", "pulseId", "electronId"} + + # all electronIds should be 0 + assert np.all(result_df.index.get_level_values("electronId") == 0) + + # pulse ids should span 0-499 on each train + assert np.all(result_df.loc[1648851402].index.get_level_values("pulseId").values + == np.arange(500)) + + # assert index uniqueness + assert result_df.index.is_unique + + # assert that dataframe contains all channels + assert np.all( + set(result_df.columns) + == set(get_channels(config_dataframe["channels"], ["per_pulse"])), + ) + + +def test_create_dataframe_per_train(config_dataframe, h5_file): + """Test the creation of a pandas DataFrame for a channel of type [per train].""" + df = DataFrameCreator(config_dataframe, h5_file) + result_df = df.df_train + + channel = "delayStage" + key, data = df.get_dataset_array(channel, slice_=True) + + # Check that the result_df is a DataFrame and has the correct shape + assert isinstance(result_df, DataFrame) + + # check that all values are in the df for delayStage + np.all(result_df[channel].dropna() == data[()]) + + # check that dataframe contains all channels + assert np.all( + set(result_df.columns) + == set(get_channels(config_dataframe["channels"], ["per_train"], extend_aux=True)), + ) + # find unique index values among all per_train channels + channels = get_channels(config_dataframe["channels"], ["per_train"]) + all_keys = Index([]) + for channel in channels: + all_keys = all_keys.append(df.get_dataset_array(channel, slice_=True)[0]) + assert result_df.shape[0] == len(all_keys.unique()) -# def test_create_dataframe_per_train(config_dataframe, h5_file): -# """ -# Test the creation of a pandas DataFrame for a channel of type [per train]. -# """ -# df = DataFrameCreator(config_dataframe, H5_FILE) -# channel = "delayStage" - -# train_id, np_array = df.create_numpy_array_per_channel(h5_file, channel) -# result_df = df.create_dataframe_per_train(np_array, train_id, channel) - -# # Check that the result_df is a DataFrame and has the correct shape -# assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == train_id.shape[0] -# assert np.all(np.equal(np.squeeze(result_df.values), np_array)) - -# assert len(result_df[result_df.index.duplicated(keep=False)]) == 0 - - -# @pytest.mark.parametrize( -# "channel", -# [ELECTRON_CHANNELS[0], "dldAux", PULSE_CHANNELS_EXTENDED[-1], TRAIN_CHANNELS[0]], -# ) -# def test_create_dataframe_per_channel(config_dataframe, h5_file, multiindex_electron, channel): -# """ -# Test the creation of a pandas Series or DataFrame for a channel from a given file. -# """ -# df = DataFrameCreator(config_dataframe, H5_FILE) -# df.index_per_electron = multiindex_electron - -# result = df.create_dataframe_per_channel(h5_file, channel) - -# # Check that the result is a Series or DataFrame and has the correct shape -# assert isinstance(result, DataFrame) - - -# def test_invalid_channel_format(config_dataframe, h5_file): -# """ -# Test ValueError for an invalid channel format. -# """ -# config = config_dataframe -# config["channels"]["dldPosX"]["format"] = "foo" - -# df = DataFrameCreator(config_dataframe, H5_FILE) - -# with pytest.raises(ValueError): -# df.create_dataframe_per_channel(h5_file, "dldPosX") - - -# def test_concatenate_channels(config_dataframe, h5_file): -# """ -# Test the concatenation of channels from an h5py.File into a pandas DataFrame. -# """ - -# df = DataFrameCreator(config_dataframe, H5_FILE) -# # Take channels for different formats as they have differing lengths -# # (train_ids can also differ) -# print(df.get_channels("all", extend_aux=False)) -# df_channels_list = [df.create_dataframe_per_channel( -# h5_file, channel).index for channel in df.get_channels("all", extend_aux=False)] -# # # print all indices -# # for i, index in enumerate(df_channels_list): -# # print(df.available_channels[i], index) -# # create union of all indices and sort them -# union_index = sorted(set().union(*df_channels_list)) -# # print(union_index) - -# result_df = df.concatenate_channels(h5_file) - -# diff_index = result_df.index.difference(union_index) -# print(diff_index) -# print(result_df.shape) -# # Check that the result_df is a DataFrame and has the correct shape -# assert isinstance(result_df, DataFrame) -# assert np.all(result_df.index == union_index) - - -# def test_group_name_not_in_h5(config_dataframe, h5_file): -# """ -# Test ValueError when the group_name for a channel does not exist in the H5 file. -# """ -# channel = "dldPosX" -# config = config_dataframe -# config["channels"][channel]["group_name"] = "foo" -# index_key = "foo" + "index" -# df = DataFrameCreator(config, H5_FILE) - -# with pytest.raises(ValueError) as e: -# df.concatenate_channels(h5_file) - -# assert str(e.value.args[0] -# ) == f"The index key: {index_key} for channel {channel} does not exist." - - -# def test_create_dataframe_per_file(config_dataframe, h5_file): -# """ -# Test the creation of pandas DataFrames for a given file. -# """ -# df = DataFrameCreator(config_dataframe, H5_FILE) -# result_df = df.create_dataframe_per_file(Path(h5_file.filename)) - -# # Check that the result_df is a DataFrame and has the correct shape -# assert isinstance(result_df, DataFrame) -# assert result_df.shape[0] == df.concatenate_channels(h5_file).shape[0] + # check index levels + assert set(result_df.index.names) == {"trainId", "pulseId", "electronId"} + + # all pulseIds and electronIds should be 0 + assert np.all(result_df.index.get_level_values("pulseId") == 0) + assert np.all(result_df.index.get_level_values("electronId") == 0) + + channel = "dldAux" + key, data = df.get_dataset_array(channel, slice_=True) + + # Check if the subchannels are correctly sliced into the dataframe + # The values are stored in DLD which is a 2D array + # The subchannels are stored in the second dimension + # Only index amount of values are stored in the first dimension, the rest are NaNs + # hence the slicing + subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] + for subchannel, index in subchannels.items(): + np.all(df.df_train[subchannel].dropna().values == data[:key.size, index]) + + assert result_df.index.is_unique + + +def test_group_name_not_in_h5(config_dataframe, h5_file): + """Test ValueError when the group_name for a channel does not exist in the H5 file.""" + channel = "dldPosX" + config = config_dataframe + config["channels"][channel]["group_name"] = "foo" + index_key = "foo" + "index" + df = DataFrameCreator(config, h5_file) + + with pytest.raises(KeyError): + df.df_electron + + +def test_create_dataframe_per_file(config_dataframe, h5_file): + """Test the creation of pandas DataFrames for a given file.""" + df = DataFrameCreator(config_dataframe, h5_file) + result_df = df.df + + # Check that the result_df is a DataFrame and has the correct shape + assert isinstance(result_df, DataFrame) + all_keys = df.df_train.index.append(df.df_electron.index).append(df.df_pulse.index) + all_keys = all_keys.unique() + assert result_df.shape[0] == len(all_keys.unique()) + + +def test_get_index_dataset_key(config_dataframe, h5_file): + """Test the creation of the index and dataset keys for a given channel.""" + config = config_dataframe + channel = "dldPosX" + df = DataFrameCreator(config, h5_file) + index_key, dataset_key = df.get_index_dataset_key(channel) + group_name = config["channels"][channel]["group_name"] + assert index_key == group_name + "index" + assert dataset_key == group_name + "value" + + # remove group_name key + del config["channels"][channel]["group_name"] + with pytest.raises(ValueError): + df.get_index_dataset_key(channel) From 77bf46bae19ad355b0c1cf65ad916e6a85cc4a7c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 18:07:34 +0100 Subject: [PATCH 027/300] add tests --- sed/loader/fel/__init__.py | 4 +- sed/loader/fel/buffer.py | 23 ++++++---- sed/loader/flash/loader.py | 4 +- tests/loader/flash/conftest.py | 22 ++++++++- tests/loader/flash/test_flash_loader.py | 60 ------------------------- 5 files changed, 38 insertions(+), 75 deletions(-) diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py index 5761a9f0..1141faa4 100644 --- a/sed/loader/fel/__init__.py +++ b/sed/loader/fel/__init__.py @@ -1,11 +1,11 @@ """sed.loader.fel module easy access APIs """ -from .buffer import BufferFileHandler +from .buffer import BufferHandler from .dataframe import DataFrameCreator from .parquet import ParquetHandler __all__ = [ - "BufferFileHandler", + "BufferHandler", "DataFrameCreator", "ParquetHandler", ] diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index 68e847b7..a061c9cc 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -32,7 +32,7 @@ from sed.loader.utils import split_dld_time_from_sector_id -class BufferFileHandler: +class BufferHandler: """ A class for handling the creation and manipulation of buffer files using DataFrameCreator and ParquetHandler. @@ -47,6 +47,7 @@ def __init__( prefix: str = "", suffix: str = "", debug: bool = False, + auto: bool = True, ) -> None: """ Initializes the BufferFileHandler. @@ -59,6 +60,7 @@ def __init__( prefix (str): Prefix for buffer file names. suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode. + auto (bool): Flag to automatically create buffer files and fill the dataframe. """ self._config = cfg_df @@ -69,14 +71,16 @@ def __init__( self.dataframe_electron: ddf.DataFrame = None self.dataframe_pulse: ddf.DataFrame = None - if not force_recreate: - self.schema_check() + # In auto mode, these methods are called automatically + if auto: + self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) - self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + if not force_recreate: + self.schema_check() - self.create_buffer_files(debug) + self.create_buffer_files(debug) - self.get_filled_dataframe() + self.get_filled_dataframe() def schema_check(self) -> None: """ @@ -218,12 +222,13 @@ def get_filled_dataframe(self) -> None: # Drop rows with nan values in the tof column tof_column = self._config.get("tof_column", "dldTimeSteps") - dataframe_electron = dataframe.dropna(subset=self._config.get(tof_column)) + dataframe_electron = dataframe.dropna(subset=tof_column) # Set the dtypes of the channels here as there should be no null values ch_dtypes = get_channels(self._config["channels"], "all") - dtypes = {channel: self._config["channels"][channel].get( - "dtype") for channel in ch_dtypes if self._config["channels"][channel].get("dtype") is not None} + cfg_ch = self._config["channels"] + dtypes = {channel: cfg_ch[channel].get( + "dtype") for channel in ch_dtypes if cfg_ch[channel].get("dtype") is not None} # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 08ee4856..fb0b8e77 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -17,7 +17,7 @@ from natsort import natsorted from sed.loader.base.loader import BaseLoader -from sed.loader.fel import BufferFileHandler +from sed.loader.fel import BufferHandler from sed.loader.fel import ParquetHandler from sed.loader.flash.metadata import MetadataRetriever @@ -279,7 +279,7 @@ def read_dataframe( # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] - buffer = BufferFileHandler( + buffer = BufferHandler( self._config["dataframe"], h5_paths, parquet_path, diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 8bf0c881..67c5c046 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -9,11 +9,20 @@ from sed.core.config import parse_config -# from sed.loader.fel import DataFrameCreator - package_dir = os.path.dirname(find_spec("sed").origin) config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" +H5_PATHS = [H5_PATH, "FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5"] + + +@pytest.fixture(name="config") +def fixture_config_file(): + """Fixture providing a configuration file for FlashLoader tests. + + Returns: + dict: The parsed configuration file. + """ + return parse_config(config_path) @pytest.fixture(name="config_dataframe") @@ -52,6 +61,15 @@ def fixture_h5_file_copy(tmp_path): return h5py.File(copy_file_path, "r+") +@pytest.fixture(name="h5_paths") +def fixture_h5_paths(): + """Fixture providing a list of h5 file paths. + + Returns: + list: A list of h5 file paths. + """ + return [os.path.join(package_dir, f"../tests/data/loader/flash/{path}") for path in H5_PATHS] + # @pytest.fixture(name="pulserSignAdc_channel_array") # def get_pulse_channel_from_h5(config_dataframe, h5_file): # df = DataFrameCreator(config_dataframe) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 88ed9e8e..f68a9d00 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -80,63 +80,3 @@ def test_initialize_paths_filenotfound(config_file: dict) -> None: fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): _, _ = fl.initialize_paths() - - -# def test_buffer_schema_mismatch(config_file): -# """ -# Test function to verify schema mismatch handling in the FlashLoader's 'read_dataframe' method. - -# The test validates the error handling mechanism when the available channels do not match the -# schema of the existing parquet files. - -# Test Steps: -# - Attempt to read a dataframe after adding a new channel 'gmdTunnel2' to the configuration. -# - Check for an expected error related to the mismatch between available channels and schema. -# - Force recreation of dataframe with the added channel, ensuring successful dataframe -# creation. -# - Simulate a missing channel scenario by removing 'gmdTunnel2' from the configuration. -# - Check for an error indicating a missing channel in the configuration. -# - Clean up created buffer files after the test. -# """ -# fl = FlashLoader(config=config_file) - -# # Read a dataframe for a specific run -# fl.read_dataframe(runs=["43878"]) - -# # Manipulate the configuration to introduce a new channel 'gmdTunnel2' -# config = config_file -# config["dataframe"]["channels"]["gmdTunnel2"] = { -# "group_name": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/", -# "format": "per_pulse", -# } - -# # Reread the dataframe with the modified configuration, expecting a schema mismatch error -# fl = FlashLoader(config=config) -# with pytest.raises(ValueError) as e: -# fl.read_dataframe(runs=["43878"]) -# expected_error = e.value.args - -# # Validate the specific error messages for schema mismatch -# assert "The available channels do not match the schema of file" in expected_error[0] -# assert expected_error[2] == "Missing in parquet: {'gmdTunnel2'}" -# assert expected_error[4] == "Please check the configuration file or set force_recreate to -# True." - -# # Force recreation of the dataframe, including the added channel 'gmdTunnel2' -# fl.read_dataframe(runs=["43878"], force_recreate=True) - -# # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario -# del config["dataframe"]["channels"]["gmdTunnel2"] -# fl = FlashLoader(config=config) -# with pytest.raises(ValueError) as e: -# # Attempt to read the dataframe again to check for the missing channel error -# fl.read_dataframe(runs=["43878"]) - -# expected_error = e.value.args -# # Check for the specific error message indicating a missing channel in the configuration -# assert expected_error[3] == "Missing in config: {'gmdTunnel2'}" - -# # Clean up created buffer files after the test -# _, parquet_data_dir = fl.initialize_paths() -# for file in os.listdir(Path(parquet_data_dir, "buffer")): -# os.remove(Path(parquet_data_dir, "buffer", file)) From d8cc6f651c155994585a4259e4100e8ca86b0d5f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 18:08:13 +0100 Subject: [PATCH 028/300] buffer handler tests --- tests/loader/flash/test_buffer_handler.py | 145 ++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 tests/loader/flash/test_buffer_handler.py diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py new file mode 100644 index 00000000..d8d59517 --- /dev/null +++ b/tests/loader/flash/test_buffer_handler.py @@ -0,0 +1,145 @@ + +from pathlib import Path +import numpy as np +import pytest +import pandas as pd +from sed.loader.fel import BufferHandler +from sed.loader.fel.utils import get_channels + + +def create_parquet_dir(config, folder): + parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) + parquet_path = parquet_path.joinpath(folder) + parquet_path.mkdir(parents=True, exist_ok=True) + return parquet_path + + +def test_get_files_to_read(config, h5_paths): + folder = create_parquet_dir(config, "get_files_to_read") + subfolder = folder.joinpath("buffer") + # set to false to avoid creating buffer files unnecessarily + bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=False) + bh.get_files_to_read(h5_paths, folder, "", "", False) + + assert bh.num_files == len(h5_paths) + assert len(bh.buffer_to_create) == len(h5_paths) + + assert np.all(bh.h5_to_create == h5_paths) + + # create expected paths + expected_buffer_paths = [ + Path(subfolder, f"{Path(path).stem}") for path in h5_paths] + + assert np.all(bh.buffer_to_create == expected_buffer_paths) + + # create only one buffer file + bh._create_buffer_file(h5_paths[0], expected_buffer_paths[0]) + # check again for files to read + bh.get_files_to_read(h5_paths, folder, "", "", False) + # check that only one file is to be read + assert bh.num_files == len(h5_paths) - 1 + Path(expected_buffer_paths[0]).unlink() # remove buffer file + + # add prefix and suffix + bh.get_files_to_read(h5_paths, folder, "prefix_", "_suffix", False) + + # expected buffer paths with prefix and suffix + expected_buffer_paths = [ + Path(subfolder, f"prefix_{Path(path).stem}_suffix") + for path in h5_paths + ] + assert np.all(bh.buffer_to_create == expected_buffer_paths) + + +def test_buffer_schema_mismatch(config, h5_paths): + """ + Test function to verify schema mismatch handling in the FlashLoader's 'read_dataframe' method. + + The test validates the error handling mechanism when the available channels do not match the + schema of the existing parquet files. + + Test Steps: + - Attempt to read a dataframe after adding a new channel 'gmdTunnel2' to the configuration. + - Check for an expected error related to the mismatch between available channels and schema. + - Force recreation of dataframe with the added channel, ensuring successful dataframe + creation. + - Simulate a missing channel scenario by removing 'gmdTunnel2' from the configuration. + - Check for an error indicating a missing channel in the configuration. + - Clean up created buffer files after the test. + """ + folder = create_parquet_dir(config, "schema_mismatch") + bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + + # Manipulate the configuration to introduce a new channel 'gmdTunnel2' + config_dict = config + config_dict["dataframe"]["channels"]["gmdTunnel2"] = { + "group_name": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/", + "format": "per_pulse", + "slice": 0, + } + + # Reread the dataframe with the modified configuration, expecting a schema mismatch error + with pytest.raises(ValueError) as e: + bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + expected_error = e.value.args + + # Validate the specific error messages for schema mismatch + assert "The available channels do not match the schema of file" in expected_error[0] + assert expected_error[2] == "Missing in parquet: {'gmdTunnel2'}" + assert expected_error[4] == "Please check the configuration file or set force_recreate to True." + + # Force recreation of the dataframe, including the added channel 'gmdTunnel2' + bh = BufferHandler(config["dataframe"], h5_paths, folder, + auto=True, force_recreate=True, debug=True) + + # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario + del config["dataframe"]["channels"]["gmdTunnel2"] + # also results in error but different from before + with pytest.raises(ValueError) as e: + # Attempt to read the dataframe again to check for the missing channel error + bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + + expected_error = e.value.args + # Check for the specific error message indicating a missing channel in the configuration + assert expected_error[3] == "Missing in config: {'gmdTunnel2'}" + + # Clean up created buffer files after the test + [path.unlink() for path in bh.buffer_paths] + + +def test_create_buffer_files(config, h5_paths): + folder_serial = create_parquet_dir(config, "create_buffer_files_serial") + bh_serial = BufferHandler(config["dataframe"], h5_paths, folder_serial, debug=True) + + folder_parallel = create_parquet_dir(config, "create_buffer_files_parallel") + bh_parallel = BufferHandler(config["dataframe"], h5_paths, folder_parallel) + + df_serial = pd.read_parquet(folder_serial) + df_parallel = pd.read_parquet(folder_parallel) + + pd.testing.assert_frame_equal(df_serial, df_parallel) + + # remove buffer files + [path.unlink() for path in bh_serial.buffer_paths] + [path.unlink() for path in bh_parallel.buffer_paths] + + +def test_get_filled_dataframe(config, h5_paths): + """ Test function to verify the creation of a filled dataframe from the buffer files. + """ + folder = create_parquet_dir(config, "get_filled_dataframe") + bh = BufferHandler(config["dataframe"], h5_paths, folder) + + df = pd.read_parquet(folder) + + assert np.all(list(bh.dataframe_electron.columns) == list(df.columns) + ["dldSectorID"]) + + channel_pulse = get_channels( + config["dataframe"]["channels"], + formats=["per_pulse", "per_train"], + index=True, + extend_aux=True, + ) + assert np.all(list(bh.dataframe_pulse.columns) == channel_pulse) + # remove buffer files + [path.unlink() for path in bh.buffer_paths] From 09cffec0810d1cee71c3bd429aaa77e92c75fc4c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 18:13:08 +0100 Subject: [PATCH 029/300] ruff formated --- .pre-commit-config.yaml | 4 +- sed/loader/fel/buffer.py | 9 +++-- sed/loader/fel/dataframe.py | 27 +++++++++---- tests/loader/flash/conftest.py | 1 + tests/loader/flash/test_buffer_handler.py | 24 +++++++----- tests/loader/flash/test_dataframe_creator.py | 41 ++++++++++++-------- 6 files changed, 67 insertions(+), 39 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f535dab..cef05707 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,10 +15,10 @@ repos: # Ruff version. rev: v0.2.2 hooks: - # Run the formatter. - - id: ruff-format # Run the linter. - id: ruff + # Run the formatter. + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.7.1 hooks: diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py index a061c9cc..a289405b 100644 --- a/sed/loader/fel/buffer.py +++ b/sed/loader/fel/buffer.py @@ -227,8 +227,11 @@ def get_filled_dataframe(self) -> None: # Set the dtypes of the channels here as there should be no null values ch_dtypes = get_channels(self._config["channels"], "all") cfg_ch = self._config["channels"] - dtypes = {channel: cfg_ch[channel].get( - "dtype") for channel in ch_dtypes if cfg_ch[channel].get("dtype") is not None} + dtypes = { + channel: cfg_ch[channel].get("dtype") + for channel in ch_dtypes + if cfg_ch[channel].get("dtype") is not None + } # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): @@ -236,7 +239,5 @@ def get_filled_dataframe(self) -> None: dataframe_electron, config=self._config, ) - else: - dataframe_electron = dataframe_electron self.dataframe_electron = dataframe_electron.astype(dtypes) self.dataframe_pulse = dataframe[index + channels] diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py index 4130ba4d..fbbbfabe 100644 --- a/sed/loader/fel/dataframe.py +++ b/sed/loader/fel/dataframe.py @@ -140,8 +140,13 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: # Final index constructed here index = MultiIndex.from_arrays( - (microbunches.get_level_values(0), microbunches.get_level_values(1).astype(int), electrons), - names=self.multi_index,) + ( + microbunches.get_level_values(0), + microbunches.get_level_values(1).astype(int), + electrons, + ), + names=self.multi_index, + ) return index, indexer @property @@ -167,13 +172,17 @@ def df_electron(self) -> DataFrame: # If all dataset keys are the same, we can directly use the ndarray to create frame if all_keys_same: _, dataset = self.get_dataset_array(channels[0]) - data_dict = {channel: dataset[:, slice_, :].ravel() - for channel, slice_ in zip(channels, slice_index)} + data_dict = { + channel: dataset[:, slice_, :].ravel() + for channel, slice_ in zip(channels, slice_index) + } dataframe = DataFrame(data_dict) # Otherwise, we need to create a Series for each channel and concatenate them else: - series = {channel: Series(self.get_dataset_array(channel, slice_=True)[ - 1].ravel()) for channel in channels} + series = { + channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + for channel in channels + } dataframe = concat(series, axis=1) drop_vals = np.arange(-offset, 0) @@ -183,7 +192,11 @@ def df_electron(self) -> DataFrame: # if necessary, the data is sorted with [indexer] # MultiIndex is set # Finally, the offset values are dropped - return dataframe.dropna()[indexer].set_index(index).drop(index=drop_vals, level="pulseId", errors="ignore") + return ( + dataframe.dropna()[indexer] + .set_index(index) + .drop(index=drop_vals, level="pulseId", errors="ignore") + ) @property def df_pulse(self) -> DataFrame: diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 67c5c046..9b7dda03 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -70,6 +70,7 @@ def fixture_h5_paths(): """ return [os.path.join(package_dir, f"../tests/data/loader/flash/{path}") for path in H5_PATHS] + # @pytest.fixture(name="pulserSignAdc_channel_array") # def get_pulse_channel_from_h5(config_dataframe, h5_file): # df = DataFrameCreator(config_dataframe) diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index d8d59517..b6f6fd37 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -1,8 +1,9 @@ - from pathlib import Path + import numpy as np -import pytest import pandas as pd +import pytest + from sed.loader.fel import BufferHandler from sed.loader.fel.utils import get_channels @@ -27,8 +28,7 @@ def test_get_files_to_read(config, h5_paths): assert np.all(bh.h5_to_create == h5_paths) # create expected paths - expected_buffer_paths = [ - Path(subfolder, f"{Path(path).stem}") for path in h5_paths] + expected_buffer_paths = [Path(subfolder, f"{Path(path).stem}") for path in h5_paths] assert np.all(bh.buffer_to_create == expected_buffer_paths) @@ -45,8 +45,7 @@ def test_get_files_to_read(config, h5_paths): # expected buffer paths with prefix and suffix expected_buffer_paths = [ - Path(subfolder, f"prefix_{Path(path).stem}_suffix") - for path in h5_paths + Path(subfolder, f"prefix_{Path(path).stem}_suffix") for path in h5_paths ] assert np.all(bh.buffer_to_create == expected_buffer_paths) @@ -89,8 +88,14 @@ def test_buffer_schema_mismatch(config, h5_paths): assert expected_error[4] == "Please check the configuration file or set force_recreate to True." # Force recreation of the dataframe, including the added channel 'gmdTunnel2' - bh = BufferHandler(config["dataframe"], h5_paths, folder, - auto=True, force_recreate=True, debug=True) + bh = BufferHandler( + config["dataframe"], + h5_paths, + folder, + auto=True, + force_recreate=True, + debug=True, + ) # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario del config["dataframe"]["channels"]["gmdTunnel2"] @@ -125,8 +130,7 @@ def test_create_buffer_files(config, h5_paths): def test_get_filled_dataframe(config, h5_paths): - """ Test function to verify the creation of a filled dataframe from the buffer files. - """ + """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") bh = BufferHandler(config["dataframe"], h5_paths, folder) diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 81f93021..0adeaf5b 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -1,9 +1,10 @@ """Tests for DataFrameCreator functionality""" -from pandas import Index import h5py -import pytest -from pandas import Index, MultiIndex, DataFrame import numpy as np +import pytest +from pandas import DataFrame +from pandas import Index +from pandas import MultiIndex from sed.loader.fel import DataFrameCreator from sed.loader.fel.utils import get_channels @@ -128,11 +129,20 @@ def test_df_electron(config_dataframe, h5_file): assert ~result_df.isnull().values.any() # Check that the values are dropped for pulseId index below 0 (ubid_offset) - print(np.all(result_df.values[:5] != np.array([[556., 731., 42888.], - [549., 737., 42881.], - [671., 577., 39181.], - [671., 579., 39196.], - [714., 859., 37530.]]))) + print( + np.all( + result_df.values[:5] + != np.array( + [ + [556.0, 731.0, 42888.0], + [549.0, 737.0, 42881.0], + [671.0, 577.0, 39181.0], + [671.0, 579.0, 39196.0], + [714.0, 859.0, 37530.0], + ], + ), + ), + ) assert np.all(result_df.index.get_level_values("pulseId") >= 0) assert isinstance(result_df, DataFrame) @@ -140,8 +150,7 @@ def test_df_electron(config_dataframe, h5_file): # check that dataframe contains all subchannels assert np.all( - set(result_df.columns) - == set(get_channels(config_dataframe["channels"], ["per_electron"])), + set(result_df.columns) == set(get_channels(config_dataframe["channels"], ["per_electron"])), ) @@ -162,16 +171,16 @@ def test_create_dataframe_per_pulse(config_dataframe, h5_file): assert np.all(result_df.index.get_level_values("electronId") == 0) # pulse ids should span 0-499 on each train - assert np.all(result_df.loc[1648851402].index.get_level_values("pulseId").values - == np.arange(500)) + assert np.all( + result_df.loc[1648851402].index.get_level_values("pulseId").values == np.arange(500), + ) # assert index uniqueness assert result_df.index.is_unique # assert that dataframe contains all channels assert np.all( - set(result_df.columns) - == set(get_channels(config_dataframe["channels"], ["per_pulse"])), + set(result_df.columns) == set(get_channels(config_dataframe["channels"], ["per_pulse"])), ) @@ -219,7 +228,7 @@ def test_create_dataframe_per_train(config_dataframe, h5_file): # hence the slicing subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] for subchannel, index in subchannels.items(): - np.all(df.df_train[subchannel].dropna().values == data[:key.size, index]) + np.all(df.df_train[subchannel].dropna().values == data[: key.size, index]) assert result_df.index.is_unique @@ -248,7 +257,7 @@ def test_create_dataframe_per_file(config_dataframe, h5_file): assert result_df.shape[0] == len(all_keys.unique()) -def test_get_index_dataset_key(config_dataframe, h5_file): +def test_get_index_dataset_key_error(config_dataframe, h5_file): """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" From 0f23ddb6db777ef06153a14759dba8b958a41fb0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 21:56:27 +0100 Subject: [PATCH 030/300] add parquethandler tests --- sed/loader/fel/parquet.py | 15 ++--- tests/loader/flash/test_parquet_handler.py | 69 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 tests/loader/flash/test_parquet_handler.py diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 4cefa945..9c9f830c 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -23,9 +23,9 @@ def __init__( self, parquet_names: str | list[str] = None, folder: Path = None, - subfolder: str = None, - prefix: str = None, - suffix: str = None, + subfolder: str = "", + prefix: str = "", + suffix: str = "", extension: str = "parquet", parquet_paths: Path = None, ): @@ -64,10 +64,10 @@ def _initialize_paths( self, parquet_names: list[str], folder: Path, - subfolder: str = "", - prefix: str = "", - suffix: str = "", - extension: str = "", + subfolder: str = None, + prefix: str = None, + suffix: str = None, + extension: str = None, ) -> None: """ Create the directory for the Parquet file. @@ -96,6 +96,7 @@ def save_parquet( drop_index (bool): If True, drops the index before saving. """ # Compute the Dask DataFrame, reset the index, and save to Parquet + dfs = dfs if isinstance(dfs, list) else [dfs] for df, parquet_path in zip(dfs, self.parquet_paths): df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py new file mode 100644 index 00000000..cd0c4a22 --- /dev/null +++ b/tests/loader/flash/test_parquet_handler.py @@ -0,0 +1,69 @@ +from pathlib import Path + +import dask.dataframe as ddf +import pandas as pd +import pytest + +from sed.loader.fel import BufferHandler +from sed.loader.fel import ParquetHandler + + +def create_parquet_dir(config, folder): + parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) + parquet_path = parquet_path.joinpath(folder) + parquet_path.mkdir(parents=True, exist_ok=True) + return parquet_path + + +def test_parquet_init_error(): + """Test ParquetHandler initialization error""" + with pytest.raises(ValueError) as e: + ParquetHandler(parquet_names="test") + + assert "Please provide folder or parquet_paths." in str(e.value) + + with pytest.raises(ValueError) as e: + ParquetHandler(folder="test") + + assert "With folder, please provide parquet_names." in str(e.value) + + +def test_initialize_paths(config): + """Test ParquetHandler initialization""" + folder = create_parquet_dir(config, "parquet_init") + + ph = ParquetHandler("test", folder, extension="xyz") + assert ph.parquet_paths[0].suffix == ".xyz" + assert ph.parquet_paths[0].name == "test.xyz" + + # test prefix and suffix + ph = ParquetHandler("test", folder, prefix="prefix_", suffix="_suffix") + assert ph.parquet_paths[0].name == "prefix_test_suffix.parquet" + + # test with list of parquet_names and subfolder + ph = ParquetHandler(["test1", "test2"], folder, subfolder="subfolder") + assert ph.parquet_paths[0].parent.name == "subfolder" + assert ph.parquet_paths[0].name == "test1.parquet" + assert ph.parquet_paths[1].name == "test2.parquet" + + +def test_sav_read_parquet(config, h5_paths): + """Test ParquetHandler save and read parquet""" + # provide instead parquet_paths + folder = create_parquet_dir(config, "parquet_save") + parquet_path = folder.joinpath("test.parquet") + + ph = ParquetHandler(parquet_paths=parquet_path) + print(ph.parquet_paths) + bh = BufferHandler(config["dataframe"], h5_paths, folder) + ph.save_parquet(bh.dataframe_electron, drop_index=True) + ph.save_parquet(bh.dataframe_electron, drop_index=False) + + df = ph.read_parquet() + # check if parquet is read correctly + assert isinstance(df, ddf.DataFrame) + [path.unlink() for path in bh.buffer_paths] + parquet_path.unlink() + # Test file not found + with pytest.raises(FileNotFoundError) as e: + ph.read_parquet() From ac4f8cde88468de65273dfb5e8d8c0ba624e47bd Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 23:05:32 +0100 Subject: [PATCH 031/300] further tests --- sed/loader/fel/parquet.py | 18 ++++++++++-------- sed/loader/flash/loader.py | 8 +++++--- tests/loader/flash/test_flash_loader.py | 20 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index 9c9f830c..a4d80adb 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -100,7 +100,7 @@ def save_parquet( for df, parquet_path in zip(dfs, self.parquet_paths): df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) - def read_parquet(self) -> ddf.DataFrame: + def read_parquet(self) -> list[ddf.DataFrame]: """ Read a Dask DataFrame from the Parquet file. @@ -110,10 +110,12 @@ def read_parquet(self) -> ddf.DataFrame: Raises: FileNotFoundError: If the Parquet file does not exist. """ - try: - return ddf.read_parquet(self.parquet_paths, calculate_divisions=True) - except Exception as exc: - raise FileNotFoundError( - "The Parquet file does not exist. " - "If it is in another location, provide the correct path as parquet_path.", - ) from exc + dfs = [] + for parquet_path in self.parquet_paths: + if not parquet_path.exists(): + raise FileNotFoundError( + "The Parquet file does not exist. " + "If it is in another location, provide the correct path as parquet_path.", + ) + dfs.append(ddf.read_parquet(parquet_path)) + return dfs diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index fb0b8e77..65f88845 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -262,7 +262,7 @@ def read_dataframe( filename = "_".join(str(run) for run in self.runs) converted_str = "converted" if converted else "" # Create parquet paths for saving and loading the parquet files of df and timed_df - prq = ParquetHandler( + ph = ParquetHandler( [filename, filename + "_timed"], parquet_path, converted_str, @@ -272,7 +272,9 @@ def read_dataframe( # Check if load_parquet is flagged and then load the file if it exists if load_parquet: - df, df_timed = prq.read_parquet() + df_list = ph.read_parquet() + df = df_list[0] + df_timed = df_list[1] # Default behavior is to create the buffer files and load them else: @@ -292,7 +294,7 @@ def read_dataframe( # Save the dataframe as parquet if requested if save_parquet: - prq.save_parquet([df, df_timed], drop_index=True) + ph.save_parquet([df, df_timed], drop_index=True) metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index f68a9d00..29d8b3da 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Literal +import pandas as pd import pytest from sed.core.config import parse_config @@ -65,6 +66,14 @@ def test_initialize_paths( assert expected_raw_path == data_raw_dir[0] assert expected_processed_path == data_parquet_dir + # remove breamtimeid, year and daq from config to raise error + del config["core"]["beamtime_id"] + fl = FlashLoader(config=config) + with pytest.raises(ValueError) as e: + _, _ = fl.initialize_paths() + + assert "The beamtime_id, year and daq are required." in str(e.value) + def test_initialize_paths_filenotfound(config_file: dict) -> None: """ @@ -80,3 +89,14 @@ def test_initialize_paths_filenotfound(config_file: dict) -> None: fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): _, _ = fl.initialize_paths() + + +def test_save_read_parquet_flash(config): + """Test ParquetHandler save and read parquet""" + fl = FlashLoader(config=config) + df1, df_timed1, _ = fl.read_dataframe(runs=[43878, 43879], save_parquet=True) + + df2, df_timed2, _ = fl.read_dataframe(runs=[43878, 43879], load_parquet=True) + + # check if parquet read is same as parquet saved read correctly + pd.testing.assert_frame_equal(df1.compute().reset_index(drop=True), df2.compute()) From 151975244cac26d2bde14dda19101c19150a29b2 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 23:13:55 +0100 Subject: [PATCH 032/300] fixes --- tests/loader/flash/test_flash_loader.py | 10 +++++++--- tests/loader/flash/test_parquet_handler.py | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 29d8b3da..096e9454 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -93,10 +93,14 @@ def test_initialize_paths_filenotfound(config_file: dict) -> None: def test_save_read_parquet_flash(config): """Test ParquetHandler save and read parquet""" - fl = FlashLoader(config=config) - df1, df_timed1, _ = fl.read_dataframe(runs=[43878, 43879], save_parquet=True) + config_ = config + config_["core"]["paths"]["data_parquet_dir"] = ( + config_["core"]["paths"]["data_parquet_dir"] + "_flash_save_read/" + ) + fl = FlashLoader(config=config_) + df1, _, _ = fl.read_dataframe(runs=[43878, 43879], save_parquet=True) - df2, df_timed2, _ = fl.read_dataframe(runs=[43878, 43879], load_parquet=True) + df2, _, _ = fl.read_dataframe(runs=[43878, 43879], load_parquet=True) # check if parquet read is same as parquet saved read correctly pd.testing.assert_frame_equal(df1.compute().reset_index(drop=True), df2.compute()) diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index cd0c4a22..ec579937 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -47,7 +47,7 @@ def test_initialize_paths(config): assert ph.parquet_paths[1].name == "test2.parquet" -def test_sav_read_parquet(config, h5_paths): +def test_save_read_parquet(config, h5_paths): """Test ParquetHandler save and read parquet""" # provide instead parquet_paths folder = create_parquet_dir(config, "parquet_save") @@ -57,11 +57,11 @@ def test_sav_read_parquet(config, h5_paths): print(ph.parquet_paths) bh = BufferHandler(config["dataframe"], h5_paths, folder) ph.save_parquet(bh.dataframe_electron, drop_index=True) + parquet_path.unlink() ph.save_parquet(bh.dataframe_electron, drop_index=False) df = ph.read_parquet() - # check if parquet is read correctly - assert isinstance(df, ddf.DataFrame) + [path.unlink() for path in bh.buffer_paths] parquet_path.unlink() # Test file not found From d31e6b1fd6a8d2ebeed86370cf57b1439ffbfb23 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jan 2024 23:22:04 +0100 Subject: [PATCH 033/300] fix the lint error --- sed/loader/fel/parquet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py index a4d80adb..9ad19e04 100644 --- a/sed/loader/fel/parquet.py +++ b/sed/loader/fel/parquet.py @@ -85,7 +85,7 @@ def _initialize_paths( def save_parquet( self, - dfs: list[ddf.DataFrame], + dfs: ddf.DataFrame | list[ddf.DataFrame], drop_index: bool = False, ) -> None: """ From ed18a5c72a40f86170a88528e1a1a95f536899b1 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 27 Mar 2024 01:36:26 +0100 Subject: [PATCH 034/300] fix parse_metadata --- sed/loader/flash/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 65f88845..8f623519 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -160,7 +160,7 @@ def get_files_from_run_id( # Return the list of found files return [str(file.resolve()) for file in files] - def parse_metadata(self) -> dict: + def parse_metadata(self, scicat_token: str = None, **kwds) -> dict: """Uses the MetadataRetriever class to fetch metadata from scicat for each run. Returns: From ce8134fd7371aa8aa1299e802385d7864fa01bb8 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 27 Mar 2024 01:52:25 +0100 Subject: [PATCH 035/300] put everything in one file --- sed/loader/fel/__init__.py | 11 - sed/loader/fel/buffer.py | 243 -------- sed/loader/fel/dataframe.py | 278 --------- sed/loader/fel/parquet.py | 121 ---- sed/loader/flash/loader.py | 584 ++++++++++++++++++- sed/loader/{fel => flash}/utils.py | 0 tests/loader/flash/test_buffer_handler.py | 4 +- tests/loader/flash/test_dataframe_creator.py | 4 +- tests/loader/flash/test_parquet_handler.py | 6 +- tests/loader/flash/test_utils.py | 2 +- 10 files changed, 589 insertions(+), 664 deletions(-) delete mode 100644 sed/loader/fel/__init__.py delete mode 100644 sed/loader/fel/buffer.py delete mode 100644 sed/loader/fel/dataframe.py delete mode 100644 sed/loader/fel/parquet.py rename sed/loader/{fel => flash}/utils.py (100%) diff --git a/sed/loader/fel/__init__.py b/sed/loader/fel/__init__.py deleted file mode 100644 index 1141faa4..00000000 --- a/sed/loader/fel/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""sed.loader.fel module easy access APIs -""" -from .buffer import BufferHandler -from .dataframe import DataFrameCreator -from .parquet import ParquetHandler - -__all__ = [ - "BufferHandler", - "DataFrameCreator", - "ParquetHandler", -] diff --git a/sed/loader/fel/buffer.py b/sed/loader/fel/buffer.py deleted file mode 100644 index a289405b..00000000 --- a/sed/loader/fel/buffer.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -The BufferFileHandler uses the DataFrameCreator class and uses the ParquetHandler class to -manage buffer files. It provides methods for initializing paths, checking the schema, -determining the list of files to read, serializing and parallelizing the creation, and reading -all files into one Dask DataFrame. - -After initialization, the electron and timed dataframes can be accessed as: - - buffer_handler = BufferFileHandler(cfg_df, h5_paths, folder) - - buffer_handler.electron_dataframe - buffer_handler.pulse_dataframe - -Force_recreate flag forces recreation of buffer files. Useful when the schema has changed. -Debug mode serializes the creation of buffer files. -""" -from __future__ import annotations - -from itertools import compress -from pathlib import Path - -import dask.dataframe as ddf -import h5py -import pyarrow.parquet as pq -from joblib import delayed -from joblib import Parallel - -from sed.core.dfops import forward_fill_lazy -from sed.loader.fel.dataframe import DataFrameCreator -from sed.loader.fel.parquet import ParquetHandler -from sed.loader.fel.utils import get_channels -from sed.loader.utils import split_dld_time_from_sector_id - - -class BufferHandler: - """ - A class for handling the creation and manipulation of buffer files using DataFrameCreator - and ParquetHandler. - """ - - def __init__( - self, - cfg_df: dict, - h5_paths: list[Path], - folder: Path, - force_recreate: bool = False, - prefix: str = "", - suffix: str = "", - debug: bool = False, - auto: bool = True, - ) -> None: - """ - Initializes the BufferFileHandler. - - Args: - cfg_df (dict): The configuration dictionary with only the dataframe key. - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - force_recreate (bool): Flag to force recreation of buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - debug (bool): Flag to enable debug mode. - auto (bool): Flag to automatically create buffer files and fill the dataframe. - """ - self._config = cfg_df - - self.buffer_paths: list[Path] = [] - self.h5_to_create: list[Path] = [] - self.buffer_to_create: list[Path] = [] - - self.dataframe_electron: ddf.DataFrame = None - self.dataframe_pulse: ddf.DataFrame = None - - # In auto mode, these methods are called automatically - if auto: - self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) - - if not force_recreate: - self.schema_check() - - self.create_buffer_files(debug) - - self.get_filled_dataframe() - - def schema_check(self) -> None: - """ - Checks the schema of the Parquet files. - - Raises: - ValueError: If the schema of the Parquet files does not match the configuration. - """ - existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] - parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) - - for i, schema in enumerate(parquet_schemas): - schema_set = set(schema.names) - if schema_set != config_schema: - missing_in_parquet = config_schema - schema_set - missing_in_config = schema_set - config_schema - - missing_in_parquet_str = ( - f"Missing in parquet: {missing_in_parquet}" if missing_in_parquet else "" - ) - missing_in_config_str = ( - f"Missing in config: {missing_in_config}" if missing_in_config else "" - ) - - raise ValueError( - "The available channels do not match the schema of file", - f"{existing_parquet_filenames[i]}", - f"{missing_in_parquet_str}", - f"{missing_in_config_str}", - "Please check the configuration file or set force_recreate to True.", - ) - - def get_files_to_read( - self, - h5_paths: list[Path], - folder: Path, - prefix: str, - suffix: str, - force_recreate: bool, - ) -> None: - """ - Determines the list of files to read and the corresponding buffer files to create. - - Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - force_recreate (bool): Flag to force recreation of buffer files. - """ - # Getting the paths of the buffer files, with subfolder as buffer and no extension - pq_handler = ParquetHandler( - [Path(h5_path).stem for h5_path in h5_paths], - folder, - "buffer", - prefix, - suffix, - extension="", - ) - self.buffer_paths = pq_handler.parquet_paths - # read only the files that do not exist or if force_recreate is True - files_to_read = [ - force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths - ] - - # Get the list of H5 files to read and the corresponding buffer files to create - self.h5_to_create = list(compress(h5_paths, files_to_read)) - self.buffer_to_create = list(compress(self.buffer_paths, files_to_read)) - - self.num_files = len(self.h5_to_create) - - print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") - - def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: - """ - Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. - - Args: - h5_path (Path): Path to the H5 file. - parquet_path (Path): Path to the buffer file. - """ - # Open the h5 file in read mode - h5_file = h5py.File(h5_path, "r") - - # Create a DataFrameCreator instance with the configuration and the h5 file - dfc = DataFrameCreator(self._config, h5_file) - - # Get the DataFrame from the DataFrameCreator instance - df = dfc.df - - # Close the h5 file - h5_file.close() - - # Reset the index of the DataFrame and save it as a parquet file - df.reset_index().to_parquet(parquet_path) - - def create_buffer_files(self, debug: bool) -> None: - """ - Creates the buffer files. - - Args: - debug (bool): Flag to enable debug mode, which serializes the creation. - """ - if self.num_files > 0: - if debug: - for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create): - self._create_buffer_file(h5_path, parquet_path) - else: - Parallel(n_jobs=self.num_files, verbose=10)( - delayed(self._create_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create) - ) - - def get_filled_dataframe(self) -> None: - """ - Reads all parquet files into one dataframe using dask and fills NaN values. - """ - dataframe = ddf.read_parquet(self.buffer_paths, calculate_divisions=True) - metadata = [pq.read_metadata(file) for file in self.buffer_paths] - - channels: list[str] = get_channels( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) - index: list[str] = get_channels(index=True) - overlap = min(file.num_rows for file in metadata) - - print("Filling nan values...") - dataframe = forward_fill_lazy( - df=dataframe, - columns=channels, - before=overlap, - iterations=self._config.get("forward_fill_iterations", 2), - ) - - # Drop rows with nan values in the tof column - tof_column = self._config.get("tof_column", "dldTimeSteps") - dataframe_electron = dataframe.dropna(subset=tof_column) - - # Set the dtypes of the channels here as there should be no null values - ch_dtypes = get_channels(self._config["channels"], "all") - cfg_ch = self._config["channels"] - dtypes = { - channel: cfg_ch[channel].get("dtype") - for channel in ch_dtypes - if cfg_ch[channel].get("dtype") is not None - } - - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - dataframe_electron = split_dld_time_from_sector_id( - dataframe_electron, - config=self._config, - ) - self.dataframe_electron = dataframe_electron.astype(dtypes) - self.dataframe_pulse = dataframe[index + channels] diff --git a/sed/loader/fel/dataframe.py b/sed/loader/fel/dataframe.py deleted file mode 100644 index fbbbfabe..00000000 --- a/sed/loader/fel/dataframe.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -This module provides functionality for creating pandas DataFrames from HDF5 files with multiple -channels, found using get_channels method. - -The DataFrameCreator class requires a configuration dictionary with only the dataframe key and -an open h5 file. It validates if provided [index and dataset keys] or [group_name key] has -groups existing in the h5 file. -Three formats of channels are supported: [per electron], [per pulse], and [per train]. -These can be accessed using the df_electron, df_pulse, and df_train properties respectively. -The combined DataFrame can be accessed using the df property. -Typical usage example: - - df_creator = DataFrameCreator(cfg_df, h5_file) - dataframe = df_creator.df -""" -from __future__ import annotations - -import h5py -import numpy as np -from pandas import concat -from pandas import DataFrame -from pandas import Index -from pandas import MultiIndex -from pandas import Series - -from sed.loader.fel.utils import get_channels - - -class DataFrameCreator: - """ - Utility class for creating pandas DataFrames from HDF5 files with multiple channels. - """ - - def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: - """ - Initializes the DataFrameCreator class. - - Args: - cfg_df (dict): The configuration dictionary with only the dataframe key. - h5_file (h5py.File): The open h5 file. - """ - self.h5_file: h5py.File = h5_file - self.failed_files_error: list[str] = [] - self.multi_index = get_channels(index=True) - self._config = cfg_df - - def get_index_dataset_key(self, channel: str) -> tuple[str, str]: - """ - Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. - - Args: - channel (str): The name of the channel. - - Returns: - tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. - - Raises: - ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. - """ - channel_config = self._config["channels"][channel] - - if "group_name" in channel_config: - index_key = channel_config["group_name"] + "index" - if channel == "timeStamp": - dataset_key = channel_config["group_name"] + "time" - else: - dataset_key = channel_config["group_name"] + "value" - return index_key, dataset_key - if "index_key" in channel_config and "dataset_key" in channel_config: - return channel_config["index_key"], channel_config["dataset_key"] - - raise ValueError( - "For channel:", - channel, - "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", - ) - - def get_dataset_array( - self, - channel: str, - slice_: bool = False, - ) -> tuple[Index, h5py.Dataset]: - """ - Returns a numpy array for a given channel name. - - Args: - channel (str): The name of the channel. - slice_ (bool): If True, applies slicing on the dataset. - - Returns: - tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array - for the channel's data. - """ - # Get the data from the necessary h5 file and channel - index_key, dataset_key = self.get_index_dataset_key(channel) - - key = Index(self.h5_file[index_key], name="trainId") # macrobunch - dataset = self.h5_file[dataset_key] - - if slice_: - slice_index = self._config["channels"][channel].get("slice", None) - if slice_index is not None: - dataset = np.take(dataset, slice_index, axis=1) - # If np_array is size zero, fill with NaNs - if dataset.shape[0] == 0: - # Fill the np_array with NaN values of the same shape as train_id - dataset = np.full_like(key, np.nan, dtype=np.double) - - return key, dataset - - def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: - """ - Computes the index for the 'per_electron' data. - - Args: - offset (int): The offset value. - - Returns: - tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and - the indexer. - """ - # Get the pulseId and the index_train - index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) - # Repeat the index_train by the number of pulses - index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) - # Explode the pulse dataset and subtract by the ubid_offset - pulse_ravel = dataset_pulse.ravel() - offset - # Create a MultiIndex with the index_train and the pulse - microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() - - # Only sort if necessary - indexer = slice(None) - if not microbunches.is_monotonic_increasing: - microbunches, indexer = microbunches.sort_values(return_indexer=True) - - # Count the number of electrons per microbunch and create an array of electrons - electron_counts = microbunches.value_counts(sort=False).values - electrons = np.concatenate([np.arange(count) for count in electron_counts]) - - # Final index constructed here - index = MultiIndex.from_arrays( - ( - microbunches.get_level_values(0), - microbunches.get_level_values(1).astype(int), - electrons, - ), - names=self.multi_index, - ) - return index, indexer - - @property - def df_electron(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per electron]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_electron' channel's data. - """ - offset = self._config["ubid_offset"] - # Index - index, indexer = self.pulse_index(offset) - - # Data logic - channels = get_channels(self._config["channels"], "per_electron") - slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] - - # First checking if dataset keys are the same for all channels - dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] - all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) - - # If all dataset keys are the same, we can directly use the ndarray to create frame - if all_keys_same: - _, dataset = self.get_dataset_array(channels[0]) - data_dict = { - channel: dataset[:, slice_, :].ravel() - for channel, slice_ in zip(channels, slice_index) - } - dataframe = DataFrame(data_dict) - # Otherwise, we need to create a Series for each channel and concatenate them - else: - series = { - channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) - for channel in channels - } - dataframe = concat(series, axis=1) - - drop_vals = np.arange(-offset, 0) - - # Few things happen here: - # Drop all NaN values like while creating the multiindex - # if necessary, the data is sorted with [indexer] - # MultiIndex is set - # Finally, the offset values are dropped - return ( - dataframe.dropna()[indexer] - .set_index(index) - .drop(index=drop_vals, level="pulseId", errors="ignore") - ) - - @property - def df_pulse(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per pulse]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. - """ - series = [] - channels = get_channels(self._config["channels"], "per_pulse") - for channel in channels: - # get slice - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, np.arange(0, dataset.shape[1]), [0]), - names=self.multi_index, - ) - series.append(Series(dataset[()].ravel(), index=index, name=channel)) - - return concat(series, axis=1) # much faster when concatenating similarly indexed data first - - @property - def df_train(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per train]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_train' channel's data. - """ - series = [] - - channels = get_channels(self._config["channels"], "per_train") - - for channel in channels: - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, [0], [0]), - names=self.multi_index, - ) - if channel == "dldAux": - aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() - for name, slice_aux in aux_channels: - series.append(Series(dataset[: key.size, slice_aux], index, name=name)) - else: - series.append(Series(dataset, index, name=channel)) - - return concat(series, axis=1) - - def validate_channel_keys(self) -> None: - """ - Validates if the index and dataset keys for all channels in config exist in the h5 file. - - Raises: - KeyError: If the index or dataset keys do not exist in the file. - """ - for channel in self._config["channels"]: - index_key, dataset_key = self.get_index_dataset_key(channel) - if index_key not in self.h5_file: - raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") - if dataset_key not in self.h5_file: - raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") - - @property - def df(self) -> DataFrame: - """ - Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, - returning a single dataframe. - - Returns: - DataFrame: The combined pandas DataFrame. - """ - - self.validate_channel_keys() - return ( - self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") - .join(self.df_train, on=self.multi_index, how="outer") - .sort_index() - ) diff --git a/sed/loader/fel/parquet.py b/sed/loader/fel/parquet.py deleted file mode 100644 index 9ad19e04..00000000 --- a/sed/loader/fel/parquet.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -The ParquetHandler class allows for saving and reading Dask DataFrames to/from Parquet files. -It also provides methods for initializing paths, saving Parquet files and also reading them -into a Dask DataFrame. - -Typical usage example: - - parquet_handler = ParquetHandler(parquet_names='data', folder=Path('/path/to/folder')) - parquet_handler.save_parquet(df) # df is a uncomputed Dask DataFrame - data = parquet_handler.read_parquet() -""" -from __future__ import annotations - -from pathlib import Path - -import dask.dataframe as ddf - - -class ParquetHandler: - """A class for handling the creation and manipulation of Parquet files.""" - - def __init__( - self, - parquet_names: str | list[str] = None, - folder: Path = None, - subfolder: str = "", - prefix: str = "", - suffix: str = "", - extension: str = "parquet", - parquet_paths: Path = None, - ): - """ - A handler for saving and reading Dask DataFrames to/from Parquet files. - - Args: - parquet_names Union[str, List[str]]: The base name of the Parquet files. - folder (Path): The directory where the Parquet file will be stored. - subfolder (str): Optional subfolder within the main folder. - prefix (str): Optional prefix for the Parquet file name. - suffix (str): Optional suffix for the Parquet file name. - parquet_path (Path): Optional custom path for the Parquet file. - """ - - self.parquet_paths: list[Path] = None - - if isinstance(parquet_names, str): - parquet_names = [parquet_names] - - if not folder and not parquet_paths: - raise ValueError("Please provide folder or parquet_paths.") - if folder and not parquet_names: - raise ValueError("With folder, please provide parquet_names.") - - # If parquet_paths is provided, use it and ignore the other arguments - # Else, initialize the paths - if parquet_paths: - self.parquet_paths = ( - parquet_paths if isinstance(parquet_paths, list) else [parquet_paths] - ) - else: - self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) - - def _initialize_paths( - self, - parquet_names: list[str], - folder: Path, - subfolder: str = None, - prefix: str = None, - suffix: str = None, - extension: str = None, - ) -> None: - """ - Create the directory for the Parquet file. - """ - # Create the full path for the Parquet file - parquet_dir = folder.joinpath(subfolder) - parquet_dir.mkdir(parents=True, exist_ok=True) - - if extension: - extension = f".{extension}" # to be backwards compatible - self.parquet_paths = [ - parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) - for name in parquet_names - ] - - def save_parquet( - self, - dfs: ddf.DataFrame | list[ddf.DataFrame], - drop_index: bool = False, - ) -> None: - """ - Save the DataFrame to a Parquet file. - - Args: - dfs (DataFrame | ddf.DataFrame): The pandas or Dask Dataframe to be saved. - drop_index (bool): If True, drops the index before saving. - """ - # Compute the Dask DataFrame, reset the index, and save to Parquet - dfs = dfs if isinstance(dfs, list) else [dfs] - for df, parquet_path in zip(dfs, self.parquet_paths): - df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) - - def read_parquet(self) -> list[ddf.DataFrame]: - """ - Read a Dask DataFrame from the Parquet file. - - Returns: - ddf.DataFrame: The Dask DataFrame read from the Parquet file. - - Raises: - FileNotFoundError: If the Parquet file does not exist. - """ - dfs = [] - for parquet_path in self.parquet_paths: - if not parquet_path.exists(): - raise FileNotFoundError( - "The Parquet file does not exist. " - "If it is in another location, provide the correct path as parquet_path.", - ) - dfs.append(ddf.read_parquet(parquet_path)) - return dfs diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 8f623519..77e48415 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -10,16 +10,596 @@ from __future__ import annotations import time +from itertools import compress from pathlib import Path from typing import Sequence import dask.dataframe as dd +import h5py +import numpy as np +import pyarrow.parquet as pq +from joblib import delayed +from joblib import Parallel from natsort import natsorted +from pandas import concat +from pandas import DataFrame +from pandas import Index +from pandas import MultiIndex +from pandas import Series +from sed.core.dfops import forward_fill_lazy from sed.loader.base.loader import BaseLoader -from sed.loader.fel import BufferHandler -from sed.loader.fel import ParquetHandler from sed.loader.flash.metadata import MetadataRetriever +from sed.loader.flash.utils import get_channels +from sed.loader.utils import split_dld_time_from_sector_id + + +class DataFrameCreator: + """ + Utility class for creating pandas DataFrames from HDF5 files with multiple channels. + """ + + def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: + """ + Initializes the DataFrameCreator class. + + Args: + cfg_df (dict): The configuration dictionary with only the dataframe key. + h5_file (h5py.File): The open h5 file. + """ + self.h5_file: h5py.File = h5_file + self.failed_files_error: list[str] = [] + self.multi_index = get_channels(index=True) + self._config = cfg_df + + def get_index_dataset_key(self, channel: str) -> tuple[str, str]: + """ + Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. + + Args: + channel (str): The name of the channel. + + Returns: + tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. + + Raises: + ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + """ + channel_config = self._config["channels"][channel] + + if "group_name" in channel_config: + index_key = channel_config["group_name"] + "index" + if channel == "timeStamp": + dataset_key = channel_config["group_name"] + "time" + else: + dataset_key = channel_config["group_name"] + "value" + return index_key, dataset_key + if "index_key" in channel_config and "dataset_key" in channel_config: + return channel_config["index_key"], channel_config["dataset_key"] + + raise ValueError( + "For channel:", + channel, + "Provide either both 'index_key' and 'dataset_key'.", + "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", + ) + + def get_dataset_array( + self, + channel: str, + slice_: bool = False, + ) -> tuple[Index, h5py.Dataset]: + """ + Returns a numpy array for a given channel name. + + Args: + channel (str): The name of the channel. + slice_ (bool): If True, applies slicing on the dataset. + + Returns: + tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array + for the channel's data. + """ + # Get the data from the necessary h5 file and channel + index_key, dataset_key = self.get_index_dataset_key(channel) + + key = Index(self.h5_file[index_key], name="trainId") # macrobunch + dataset = self.h5_file[dataset_key] + + if slice_: + slice_index = self._config["channels"][channel].get("slice", None) + if slice_index is not None: + dataset = np.take(dataset, slice_index, axis=1) + # If np_array is size zero, fill with NaNs + if dataset.shape[0] == 0: + # Fill the np_array with NaN values of the same shape as train_id + dataset = np.full_like(key, np.nan, dtype=np.double) + + return key, dataset + + def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: + """ + Computes the index for the 'per_electron' data. + + Args: + offset (int): The offset value. + + Returns: + tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and + the indexer. + """ + # Get the pulseId and the index_train + index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) + # Repeat the index_train by the number of pulses + index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) + # Explode the pulse dataset and subtract by the ubid_offset + pulse_ravel = dataset_pulse.ravel() - offset + # Create a MultiIndex with the index_train and the pulse + microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + + # Only sort if necessary + indexer = slice(None) + if not microbunches.is_monotonic_increasing: + microbunches, indexer = microbunches.sort_values(return_indexer=True) + + # Count the number of electrons per microbunch and create an array of electrons + electron_counts = microbunches.value_counts(sort=False).values + electrons = np.concatenate([np.arange(count) for count in electron_counts]) + + # Final index constructed here + index = MultiIndex.from_arrays( + ( + microbunches.get_level_values(0), + microbunches.get_level_values(1).astype(int), + electrons, + ), + names=self.multi_index, + ) + return index, indexer + + @property + def df_electron(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per electron]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_electron' channel's data. + """ + offset = self._config["ubid_offset"] + # Index + index, indexer = self.pulse_index(offset) + + # Data logic + channels = get_channels(self._config["channels"], "per_electron") + slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + + # First checking if dataset keys are the same for all channels + dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] + all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + + # If all dataset keys are the same, we can directly use the ndarray to create frame + if all_keys_same: + _, dataset = self.get_dataset_array(channels[0]) + data_dict = { + channel: dataset[:, slice_, :].ravel() + for channel, slice_ in zip(channels, slice_index) + } + dataframe = DataFrame(data_dict) + # Otherwise, we need to create a Series for each channel and concatenate them + else: + series = { + channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + for channel in channels + } + dataframe = concat(series, axis=1) + + drop_vals = np.arange(-offset, 0) + + # Few things happen here: + # Drop all NaN values like while creating the multiindex + # if necessary, the data is sorted with [indexer] + # MultiIndex is set + # Finally, the offset values are dropped + return ( + dataframe.dropna()[indexer] + .set_index(index) + .drop(index=drop_vals, level="pulseId", errors="ignore") + ) + + @property + def df_pulse(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per pulse]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. + """ + series = [] + channels = get_channels(self._config["channels"], "per_pulse") + for channel in channels: + # get slice + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, np.arange(0, dataset.shape[1]), [0]), + names=self.multi_index, + ) + series.append(Series(dataset[()].ravel(), index=index, name=channel)) + + return concat(series, axis=1) # much faster when concatenating similarly indexed data first + + @property + def df_train(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per train]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_train' channel's data. + """ + series = [] + + channels = get_channels(self._config["channels"], "per_train") + + for channel in channels: + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, [0], [0]), + names=self.multi_index, + ) + if channel == "dldAux": + aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() + for name, slice_aux in aux_channels: + series.append(Series(dataset[: key.size, slice_aux], index, name=name)) + else: + series.append(Series(dataset, index, name=channel)) + + return concat(series, axis=1) + + def validate_channel_keys(self) -> None: + """ + Validates if the index and dataset keys for all channels in config exist in the h5 file. + + Raises: + KeyError: If the index or dataset keys do not exist in the file. + """ + for channel in self._config["channels"]: + index_key, dataset_key = self.get_index_dataset_key(channel) + if index_key not in self.h5_file: + raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") + if dataset_key not in self.h5_file: + raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") + + @property + def df(self) -> DataFrame: + """ + Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, + returning a single dataframe. + + Returns: + DataFrame: The combined pandas DataFrame. + """ + + self.validate_channel_keys() + return ( + self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") + .join(self.df_train, on=self.multi_index, how="outer") + .sort_index() + ) + + +class BufferHandler: + """ + A class for handling the creation and manipulation of buffer files using DataFrameCreator + and ParquetHandler. + """ + + def __init__( + self, + cfg_df: dict, + h5_paths: list[Path], + folder: Path, + force_recreate: bool = False, + prefix: str = "", + suffix: str = "", + debug: bool = False, + auto: bool = True, + ) -> None: + """ + Initializes the BufferFileHandler. + + Args: + cfg_df (dict): The configuration dictionary with only the dataframe key. + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + force_recreate (bool): Flag to force recreation of buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + debug (bool): Flag to enable debug mode. + auto (bool): Flag to automatically create buffer files and fill the dataframe. + """ + self._config = cfg_df + + self.buffer_paths: list[Path] = [] + self.h5_to_create: list[Path] = [] + self.buffer_to_create: list[Path] = [] + + self.dataframe_electron: dd.DataFrame = None + self.dataframe_pulse: dd.DataFrame = None + + # In auto mode, these methods are called automatically + if auto: + self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + + if not force_recreate: + self.schema_check() + + self.create_buffer_files(debug) + + self.get_filled_dataframe() + + def schema_check(self) -> None: + """ + Checks the schema of the Parquet files. + + Raises: + ValueError: If the schema of the Parquet files does not match the configuration. + """ + existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] + parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] + config_schema = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + + for i, schema in enumerate(parquet_schemas): + schema_set = set(schema.names) + if schema_set != config_schema: + missing_in_parquet = config_schema - schema_set + missing_in_config = schema_set - config_schema + + missing_in_parquet_str = ( + f"Missing in parquet: {missing_in_parquet}" if missing_in_parquet else "" + ) + missing_in_config_str = ( + f"Missing in config: {missing_in_config}" if missing_in_config else "" + ) + + raise ValueError( + "The available channels do not match the schema of file", + f"{existing_parquet_filenames[i]}", + f"{missing_in_parquet_str}", + f"{missing_in_config_str}", + "Please check the configuration file or set force_recreate to True.", + ) + + def get_files_to_read( + self, + h5_paths: list[Path], + folder: Path, + prefix: str, + suffix: str, + force_recreate: bool, + ) -> None: + """ + Determines the list of files to read and the corresponding buffer files to create. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + force_recreate (bool): Flag to force recreation of buffer files. + """ + # Getting the paths of the buffer files, with subfolder as buffer and no extension + pq_handler = ParquetHandler( + [Path(h5_path).stem for h5_path in h5_paths], + folder, + "buffer", + prefix, + suffix, + extension="", + ) + self.buffer_paths = pq_handler.parquet_paths + # read only the files that do not exist or if force_recreate is True + files_to_read = [ + force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths + ] + + # Get the list of H5 files to read and the corresponding buffer files to create + self.h5_to_create = list(compress(h5_paths, files_to_read)) + self.buffer_to_create = list(compress(self.buffer_paths, files_to_read)) + + self.num_files = len(self.h5_to_create) + + print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") + + def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: + """ + Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. + + Args: + h5_path (Path): Path to the H5 file. + parquet_path (Path): Path to the buffer file. + """ + # Open the h5 file in read mode + h5_file = h5py.File(h5_path, "r") + + # Create a DataFrameCreator instance with the configuration and the h5 file + dfc = DataFrameCreator(self._config, h5_file) + + # Get the DataFrame from the DataFrameCreator instance + df = dfc.df + + # Close the h5 file + h5_file.close() + + # Reset the index of the DataFrame and save it as a parquet file + df.reset_index().to_parquet(parquet_path) + + def create_buffer_files(self, debug: bool) -> None: + """ + Creates the buffer files. + + Args: + debug (bool): Flag to enable debug mode, which serializes the creation. + """ + if self.num_files > 0: + if debug: + for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create): + self._create_buffer_file(h5_path, parquet_path) + else: + Parallel(n_jobs=self.num_files, verbose=10)( + delayed(self._create_buffer_file)(h5_path, parquet_path) + for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create) + ) + + def get_filled_dataframe(self) -> None: + """ + Reads all parquet files into one dataframe using dask and fills NaN values. + """ + dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) + metadata = [pq.read_metadata(file) for file in self.buffer_paths] + + channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + index: list[str] = get_channels(index=True) + overlap = min(file.num_rows for file in metadata) + + print("Filling nan values...") + dataframe = forward_fill_lazy( + df=dataframe, + columns=channels, + before=overlap, + iterations=self._config.get("forward_fill_iterations", 2), + ) + + # Drop rows with nan values in the tof column + tof_column = self._config.get("tof_column", "dldTimeSteps") + dataframe_electron = dataframe.dropna(subset=tof_column) + + # Set the dtypes of the channels here as there should be no null values + ch_dtypes = get_channels(self._config["channels"], "all") + cfg_ch = self._config["channels"] + dtypes = { + channel: cfg_ch[channel].get("dtype") + for channel in ch_dtypes + if cfg_ch[channel].get("dtype") is not None + } + + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + dataframe_electron = split_dld_time_from_sector_id( + dataframe_electron, + config=self._config, + ) + self.dataframe_electron = dataframe_electron.astype(dtypes) + self.dataframe_pulse = dataframe[index + channels] + + +class ParquetHandler: + """A class for handling the creation and manipulation of Parquet files.""" + + def __init__( + self, + parquet_names: str | list[str] = None, + folder: Path = None, + subfolder: str = "", + prefix: str = "", + suffix: str = "", + extension: str = "parquet", + parquet_paths: Path = None, + ): + """ + A handler for saving and reading Dask DataFrames to/from Parquet files. + + Args: + parquet_names Union[str, List[str]]: The base name of the Parquet files. + folder (Path): The directory where the Parquet file will be stored. + subfolder (str): Optional subfolder within the main folder. + prefix (str): Optional prefix for the Parquet file name. + suffix (str): Optional suffix for the Parquet file name. + parquet_path (Path): Optional custom path for the Parquet file. + """ + + self.parquet_paths: list[Path] = None + + if isinstance(parquet_names, str): + parquet_names = [parquet_names] + + if not folder and not parquet_paths: + raise ValueError("Please provide folder or parquet_paths.") + if folder and not parquet_names: + raise ValueError("With folder, please provide parquet_names.") + + # If parquet_paths is provided, use it and ignore the other arguments + # Else, initialize the paths + if parquet_paths: + self.parquet_paths = ( + parquet_paths if isinstance(parquet_paths, list) else [parquet_paths] + ) + else: + self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) + + def _initialize_paths( + self, + parquet_names: list[str], + folder: Path, + subfolder: str = None, + prefix: str = None, + suffix: str = None, + extension: str = None, + ) -> None: + """ + Create the directory for the Parquet file. + """ + # Create the full path for the Parquet file + parquet_dir = folder.joinpath(subfolder) + parquet_dir.mkdir(parents=True, exist_ok=True) + + if extension: + extension = f".{extension}" # to be backwards compatible + self.parquet_paths = [ + parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) + for name in parquet_names + ] + + def save_parquet( + self, + dfs: dd.DataFrame | list[dd.DataFrame], + drop_index: bool = False, + ) -> None: + """ + Save the DataFrame to a Parquet file. + + Args: + dfs (DataFrame | dd.DataFrame): The pandas or Dask Dataframe to be saved. + drop_index (bool): If True, drops the index before saving. + """ + # Compute the Dask DataFrame, reset the index, and save to Parquet + dfs = dfs if isinstance(dfs, list) else [dfs] + for df, parquet_path in zip(dfs, self.parquet_paths): + df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) + + def read_parquet(self) -> list[dd.DataFrame]: + """ + Read a Dask DataFrame from the Parquet file. + + Returns: + dd.DataFrame: The Dask DataFrame read from the Parquet file. + + Raises: + FileNotFoundError: If the Parquet file does not exist. + """ + dfs = [] + for parquet_path in self.parquet_paths: + if not parquet_path.exists(): + raise FileNotFoundError( + "The Parquet file does not exist. " + "If it is in another location, provide the correct path as parquet_path.", + ) + dfs.append(dd.read_parquet(parquet_path)) + return dfs class FlashLoader(BaseLoader): diff --git a/sed/loader/fel/utils.py b/sed/loader/flash/utils.py similarity index 100% rename from sed/loader/fel/utils.py rename to sed/loader/flash/utils.py diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index b6f6fd37..fbe60b05 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -4,8 +4,8 @@ import pandas as pd import pytest -from sed.loader.fel import BufferHandler -from sed.loader.fel.utils import get_channels +from sed.loader.flash.loader import BufferHandler +from sed.loader.flash.utils import get_channels def create_parquet_dir(config, folder): diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 0adeaf5b..e287713f 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -6,8 +6,8 @@ from pandas import Index from pandas import MultiIndex -from sed.loader.fel import DataFrameCreator -from sed.loader.fel.utils import get_channels +from sed.loader.flash.loader import DataFrameCreator +from sed.loader.flash.utils import get_channels def test_get_index_dataset_key(config_dataframe, h5_file): diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index ec579937..76338c86 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -1,11 +1,9 @@ from pathlib import Path -import dask.dataframe as ddf -import pandas as pd import pytest -from sed.loader.fel import BufferHandler -from sed.loader.fel import ParquetHandler +from sed.loader.flash.loader import BufferHandler +from sed.loader.flash.loader import ParquetHandler def create_parquet_dir(config, folder): diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index 3773756a..efd70e89 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -1,5 +1,5 @@ """Tests for utils functionality""" -from sed.loader.fel.utils import get_channels +from sed.loader.flash.utils import get_channels # Define expected channels for each format. ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] From 08a2adcaa02f579d0f3a9299d55a9feaa1c3390c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 27 Mar 2024 01:54:42 +0100 Subject: [PATCH 036/300] reoder --- sed/loader/flash/loader.py | 1042 ++++++++++++++++++------------------ 1 file changed, 521 insertions(+), 521 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 77e48415..9f6300d4 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -34,325 +34,605 @@ from sed.loader.utils import split_dld_time_from_sector_id -class DataFrameCreator: +class FlashLoader(BaseLoader): """ - Utility class for creating pandas DataFrames from HDF5 files with multiple channels. + The class generates multiindexed multidimensional pandas dataframes from the new FLASH + dataformat resolved by both macro and microbunches alongside electrons. + Only the read_dataframe (inherited and implemented) method is accessed by other modules. """ - def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: + __name__ = "flash" + + supported_file_types = ["h5"] + + def __init__(self, config: dict) -> None: """ - Initializes the DataFrameCreator class. + Initializes the FlashLoader. Args: - cfg_df (dict): The configuration dictionary with only the dataframe key. - h5_file (h5py.File): The open h5 file. + config (dict): Configuration dictionary. """ - self.h5_file: h5py.File = h5_file - self.failed_files_error: list[str] = [] - self.multi_index = get_channels(index=True) - self._config = cfg_df + super().__init__(config=config) - def get_index_dataset_key(self, channel: str) -> tuple[str, str]: + def initialize_paths(self) -> tuple[list[Path], Path]: """ - Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. - - Args: - channel (str): The name of the channel. + Initializes the paths based on the configuration. Returns: - tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. + Tuple[List[Path], Path]: A tuple containing a list of raw data directories + paths and the parquet data directory path. Raises: - ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + ValueError: If required values are missing from the configuration. + FileNotFoundError: If the raw data directories are not found. """ - channel_config = self._config["channels"][channel] + # Parses to locate the raw beamtime directory from config file + if "paths" in self._config["core"]: + data_raw_dir = [ + Path(self._config["core"]["paths"].get("data_raw_dir", "")), + ] + data_parquet_dir = Path( + self._config["core"]["paths"].get("data_parquet_dir", ""), + ) - if "group_name" in channel_config: - index_key = channel_config["group_name"] + "index" - if channel == "timeStamp": - dataset_key = channel_config["group_name"] + "time" - else: - dataset_key = channel_config["group_name"] + "value" - return index_key, dataset_key - if "index_key" in channel_config and "dataset_key" in channel_config: - return channel_config["index_key"], channel_config["dataset_key"] + else: + try: + beamtime_id = self._config["core"]["beamtime_id"] + year = self._config["core"]["year"] + daq = self._config["dataframe"]["daq"] + except KeyError as exc: + raise ValueError( + "The beamtime_id, year and daq are required.", + ) from exc - raise ValueError( - "For channel:", - channel, - "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", - ) + beamtime_dir = Path( + self._config["dataframe"]["beamtime_dir"][self._config["core"]["beamline"]], + ) + beamtime_dir = beamtime_dir.joinpath(f"{year}/data/{beamtime_id}/") - def get_dataset_array( - self, - channel: str, - slice_: bool = False, - ) -> tuple[Index, h5py.Dataset]: - """ - Returns a numpy array for a given channel name. + # Use pathlib walk to reach the raw data directory + data_raw_dir = [] + raw_path = beamtime_dir.joinpath("raw") - Args: - channel (str): The name of the channel. - slice_ (bool): If True, applies slicing on the dataset. + for path in raw_path.glob("**/*"): + if path.is_dir(): + dir_name = path.name + if dir_name.startswith("express-") or dir_name.startswith( + "online-", + ): + data_raw_dir.append(path.joinpath(daq)) + elif dir_name == daq.upper(): + data_raw_dir.append(path) - Returns: - tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array - for the channel's data. - """ - # Get the data from the necessary h5 file and channel - index_key, dataset_key = self.get_index_dataset_key(channel) + if not data_raw_dir: + raise FileNotFoundError("Raw data directories not found.") - key = Index(self.h5_file[index_key], name="trainId") # macrobunch - dataset = self.h5_file[dataset_key] + parquet_path = "processed/parquet" + data_parquet_dir = beamtime_dir.joinpath(parquet_path) - if slice_: - slice_index = self._config["channels"][channel].get("slice", None) - if slice_index is not None: - dataset = np.take(dataset, slice_index, axis=1) - # If np_array is size zero, fill with NaNs - if dataset.shape[0] == 0: - # Fill the np_array with NaN values of the same shape as train_id - dataset = np.full_like(key, np.nan, dtype=np.double) + data_parquet_dir.mkdir(parents=True, exist_ok=True) - return key, dataset + return data_raw_dir, data_parquet_dir - def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: + def get_files_from_run_id( + self, + run_id: str, + folders: str | Sequence[str] = None, + extension: str = "h5", + **kwds, + ) -> list[str]: """ - Computes the index for the 'per_electron' data. + Returns a list of filenames for a given run located in the specified directory + for the specified data acquisition (daq). Args: - offset (int): The offset value. + run_id (str): The run identifier to locate. + folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + data is located. Defaults to config["core"]["base_folder"]. + extension (str, optional): The file extension. Defaults to "h5". + kwds: Keyword arguments: + - daq (str): The data acquisition identifier. Returns: - tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and - the indexer. - """ - # Get the pulseId and the index_train - index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) - # Repeat the index_train by the number of pulses - index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) - # Explode the pulse dataset and subtract by the ubid_offset - pulse_ravel = dataset_pulse.ravel() - offset - # Create a MultiIndex with the index_train and the pulse - microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + List[str]: A list of path strings representing the collected file names. - # Only sort if necessary - indexer = slice(None) - if not microbunches.is_monotonic_increasing: - microbunches, indexer = microbunches.sort_values(return_indexer=True) + Raises: + FileNotFoundError: If no files are found for the given run in the directory. + """ + # Define the stream name prefixes based on the data acquisition identifier + stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] - # Count the number of electrons per microbunch and create an array of electrons - electron_counts = microbunches.value_counts(sort=False).values - electrons = np.concatenate([np.arange(count) for count in electron_counts]) + if folders is None: + folders = self._config["core"]["base_folder"] - # Final index constructed here - index = MultiIndex.from_arrays( - ( - microbunches.get_level_values(0), - microbunches.get_level_values(1).astype(int), - electrons, - ), - names=self.multi_index, - ) - return index, indexer + if isinstance(folders, str): + folders = [folders] - @property - def df_electron(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per electron]. + daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) - Returns: - DataFrame: The pandas DataFrame for the 'per_electron' channel's data. - """ - offset = self._config["ubid_offset"] - # Index - index, indexer = self.pulse_index(offset) + # Generate the file patterns to search for in the directory + file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension - # Data logic - channels = get_channels(self._config["channels"], "per_electron") - slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + files: list[Path] = [] + # Use pathlib to search for matching files in each directory + for folder in folders: + files.extend( + natsorted( + Path(folder).glob(file_pattern), + key=lambda filename: str(filename).rsplit("_", maxsplit=1)[-1], + ), + ) - # First checking if dataset keys are the same for all channels - dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] - all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + # Check if any files are found + if not files: + raise FileNotFoundError( + f"No files found for run {run_id} in directory {str(folders)}", + ) - # If all dataset keys are the same, we can directly use the ndarray to create frame - if all_keys_same: - _, dataset = self.get_dataset_array(channels[0]) - data_dict = { - channel: dataset[:, slice_, :].ravel() - for channel, slice_ in zip(channels, slice_index) - } - dataframe = DataFrame(data_dict) - # Otherwise, we need to create a Series for each channel and concatenate them - else: - series = { - channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) - for channel in channels - } - dataframe = concat(series, axis=1) + # Return the list of found files + return [str(file.resolve()) for file in files] - drop_vals = np.arange(-offset, 0) + def parse_metadata(self, scicat_token: str = None, **kwds) -> dict: + """Uses the MetadataRetriever class to fetch metadata from scicat for each run. - # Few things happen here: - # Drop all NaN values like while creating the multiindex - # if necessary, the data is sorted with [indexer] - # MultiIndex is set - # Finally, the offset values are dropped - return ( - dataframe.dropna()[indexer] - .set_index(index) - .drop(index=drop_vals, level="pulseId", errors="ignore") + Returns: + dict: Metadata dictionary + scicat_token (str, optional):: The scicat token to use for fetching metadata + """ + metadata_retriever = MetadataRetriever(self._config["metadata"], scicat_token) + metadata = metadata_retriever.get_metadata( + beamtime_id=self._config["core"]["beamtime_id"], + runs=self.runs, + metadata=self.metadata, ) - @property - def df_pulse(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per pulse]. + return metadata - Returns: - DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. - """ - series = [] - channels = get_channels(self._config["channels"], "per_pulse") - for channel in channels: - # get slice - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, np.arange(0, dataset.shape[1]), [0]), - names=self.multi_index, - ) - series.append(Series(dataset[()].ravel(), index=index, name=channel)) + def get_count_rate( + self, + fids: Sequence[int] = None, + **kwds, + ): + return None, None - return concat(series, axis=1) # much faster when concatenating similarly indexed data first + def get_elapsed_time(self, fids=None, **kwds): + return None - @property - def df_train(self) -> DataFrame: + def read_dataframe( + self, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, + ftype: str = "h5", + metadata: dict = None, + collect_metadata: bool = False, + converted: bool = False, + load_parquet: bool = False, + save_parquet: bool = False, + detector: str = "", + force_recreate: bool = False, + parquet_dir: str | Path = None, + debug: bool = False, + **kwds, + ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ - Returns a pandas DataFrame for a given channel name of type [per train]. + Read express data from the DAQ, generating a parquet in between. + + Args: + files (Union[str, Sequence[str]], optional): File path(s) to process. Defaults to None. + folders (Union[str, Sequence[str]], optional): Path to folder(s) where files are stored + Path has priority such that if it's specified, the specified files will be ignored. + Defaults to None. + runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding files will + be located in the location provided by ``folders``. Takes precedence over + ``files`` and ``folders``. Defaults to None. + ftype (str, optional): The file extension type. Defaults to "h5". + metadata (dict, optional): Additional metadata. Defaults to None. + collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. Returns: - DataFrame: The pandas DataFrame for the 'per_train' channel's data. + Tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame + and metadata. + + Raises: + ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. + FileNotFoundError: If the conversion fails for some files or no data is available. """ - series = [] + t0 = time.time() - channels = get_channels(self._config["channels"], "per_train") + data_raw_dir, data_parquet_dir = self.initialize_paths() - for channel in channels: - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, [0], [0]), - names=self.multi_index, + # Prepare a list of names for the runs to read and parquets to write + if runs is not None: + files = [] + if isinstance(runs, (str, int)): + runs = [runs] + for run in runs: + run_files = self.get_files_from_run_id( + run_id=run, + folders=[str(folder.resolve()) for folder in data_raw_dir], + extension=ftype, + daq=self._config["dataframe"]["daq"], + ) + files.extend(run_files) + self.runs = list(runs) + super().read_dataframe(files=files, ftype=ftype) + + else: + # This call takes care of files and folders. As we have converted runs into files + # already, they are just stored in the class by this call. + super().read_dataframe( + files=files, + folders=folders, + ftype=ftype, + metadata=metadata, ) - if channel == "dldAux": - aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() - for name, slice_aux in aux_channels: - series.append(Series(dataset[: key.size, slice_aux], index, name=name)) - else: - series.append(Series(dataset, index, name=channel)) - return concat(series, axis=1) + # if parquet_dir is None, use data_parquet_dir + parquet_dir = parquet_dir or data_parquet_dir + parquet_path = Path(parquet_dir) + filename = "_".join(str(run) for run in self.runs) + converted_str = "converted" if converted else "" + # Create parquet paths for saving and loading the parquet files of df and timed_df + ph = ParquetHandler( + [filename, filename + "_timed"], + parquet_path, + converted_str, + "run_", + detector, + ) - def validate_channel_keys(self) -> None: - """ - Validates if the index and dataset keys for all channels in config exist in the h5 file. + # Check if load_parquet is flagged and then load the file if it exists + if load_parquet: + df_list = ph.read_parquet() + df = df_list[0] + df_timed = df_list[1] - Raises: - KeyError: If the index or dataset keys do not exist in the file. - """ - for channel in self._config["channels"]: - index_key, dataset_key = self.get_index_dataset_key(channel) - if index_key not in self.h5_file: - raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") - if dataset_key not in self.h5_file: - raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") + # Default behavior is to create the buffer files and load them + else: + # Obtain the parquet filenames, metadata, and schema from the method + # which handles buffer file creation/reading + h5_paths = [Path(file) for file in self.files] + buffer = BufferHandler( + self._config["dataframe"], + h5_paths, + parquet_path, + force_recreate, + suffix=detector, + debug=debug, + ) + df = buffer.dataframe_electron + df_timed = buffer.dataframe_pulse - @property - def df(self) -> DataFrame: - """ - Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, - returning a single dataframe. + # Save the dataframe as parquet if requested + if save_parquet: + ph.save_parquet([df, df_timed], drop_index=True) - Returns: - DataFrame: The combined pandas DataFrame. - """ + metadata = self.parse_metadata(**kwds) if collect_metadata else {} + print(f"loading complete in {time.time() - t0: .2f} s") - self.validate_channel_keys() - return ( - self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") - .join(self.df_train, on=self.multi_index, how="outer") - .sort_index() - ) + return df, df_timed, metadata -class BufferHandler: +class DataFrameCreator: """ - A class for handling the creation and manipulation of buffer files using DataFrameCreator - and ParquetHandler. + Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ - def __init__( - self, - cfg_df: dict, - h5_paths: list[Path], - folder: Path, - force_recreate: bool = False, - prefix: str = "", - suffix: str = "", - debug: bool = False, - auto: bool = True, - ) -> None: + def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: """ - Initializes the BufferFileHandler. + Initializes the DataFrameCreator class. Args: cfg_df (dict): The configuration dictionary with only the dataframe key. - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - force_recreate (bool): Flag to force recreation of buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - debug (bool): Flag to enable debug mode. - auto (bool): Flag to automatically create buffer files and fill the dataframe. + h5_file (h5py.File): The open h5 file. """ + self.h5_file: h5py.File = h5_file + self.failed_files_error: list[str] = [] + self.multi_index = get_channels(index=True) self._config = cfg_df - self.buffer_paths: list[Path] = [] - self.h5_to_create: list[Path] = [] - self.buffer_to_create: list[Path] = [] + def get_index_dataset_key(self, channel: str) -> tuple[str, str]: + """ + Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. - self.dataframe_electron: dd.DataFrame = None - self.dataframe_pulse: dd.DataFrame = None + Args: + channel (str): The name of the channel. - # In auto mode, these methods are called automatically - if auto: - self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + Returns: + tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. - if not force_recreate: - self.schema_check() + Raises: + ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + """ + channel_config = self._config["channels"][channel] - self.create_buffer_files(debug) + if "group_name" in channel_config: + index_key = channel_config["group_name"] + "index" + if channel == "timeStamp": + dataset_key = channel_config["group_name"] + "time" + else: + dataset_key = channel_config["group_name"] + "value" + return index_key, dataset_key + if "index_key" in channel_config and "dataset_key" in channel_config: + return channel_config["index_key"], channel_config["dataset_key"] - self.get_filled_dataframe() + raise ValueError( + "For channel:", + channel, + "Provide either both 'index_key' and 'dataset_key'.", + "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", + ) - def schema_check(self) -> None: + def get_dataset_array( + self, + channel: str, + slice_: bool = False, + ) -> tuple[Index, h5py.Dataset]: """ - Checks the schema of the Parquet files. + Returns a numpy array for a given channel name. - Raises: - ValueError: If the schema of the Parquet files does not match the configuration. + Args: + channel (str): The name of the channel. + slice_ (bool): If True, applies slicing on the dataset. + + Returns: + tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array + for the channel's data. """ - existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] - parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) + # Get the data from the necessary h5 file and channel + index_key, dataset_key = self.get_index_dataset_key(channel) - for i, schema in enumerate(parquet_schemas): - schema_set = set(schema.names) - if schema_set != config_schema: - missing_in_parquet = config_schema - schema_set + key = Index(self.h5_file[index_key], name="trainId") # macrobunch + dataset = self.h5_file[dataset_key] + + if slice_: + slice_index = self._config["channels"][channel].get("slice", None) + if slice_index is not None: + dataset = np.take(dataset, slice_index, axis=1) + # If np_array is size zero, fill with NaNs + if dataset.shape[0] == 0: + # Fill the np_array with NaN values of the same shape as train_id + dataset = np.full_like(key, np.nan, dtype=np.double) + + return key, dataset + + def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: + """ + Computes the index for the 'per_electron' data. + + Args: + offset (int): The offset value. + + Returns: + tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and + the indexer. + """ + # Get the pulseId and the index_train + index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) + # Repeat the index_train by the number of pulses + index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) + # Explode the pulse dataset and subtract by the ubid_offset + pulse_ravel = dataset_pulse.ravel() - offset + # Create a MultiIndex with the index_train and the pulse + microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + + # Only sort if necessary + indexer = slice(None) + if not microbunches.is_monotonic_increasing: + microbunches, indexer = microbunches.sort_values(return_indexer=True) + + # Count the number of electrons per microbunch and create an array of electrons + electron_counts = microbunches.value_counts(sort=False).values + electrons = np.concatenate([np.arange(count) for count in electron_counts]) + + # Final index constructed here + index = MultiIndex.from_arrays( + ( + microbunches.get_level_values(0), + microbunches.get_level_values(1).astype(int), + electrons, + ), + names=self.multi_index, + ) + return index, indexer + + @property + def df_electron(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per electron]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_electron' channel's data. + """ + offset = self._config["ubid_offset"] + # Index + index, indexer = self.pulse_index(offset) + + # Data logic + channels = get_channels(self._config["channels"], "per_electron") + slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + + # First checking if dataset keys are the same for all channels + dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] + all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + + # If all dataset keys are the same, we can directly use the ndarray to create frame + if all_keys_same: + _, dataset = self.get_dataset_array(channels[0]) + data_dict = { + channel: dataset[:, slice_, :].ravel() + for channel, slice_ in zip(channels, slice_index) + } + dataframe = DataFrame(data_dict) + # Otherwise, we need to create a Series for each channel and concatenate them + else: + series = { + channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + for channel in channels + } + dataframe = concat(series, axis=1) + + drop_vals = np.arange(-offset, 0) + + # Few things happen here: + # Drop all NaN values like while creating the multiindex + # if necessary, the data is sorted with [indexer] + # MultiIndex is set + # Finally, the offset values are dropped + return ( + dataframe.dropna()[indexer] + .set_index(index) + .drop(index=drop_vals, level="pulseId", errors="ignore") + ) + + @property + def df_pulse(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per pulse]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. + """ + series = [] + channels = get_channels(self._config["channels"], "per_pulse") + for channel in channels: + # get slice + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, np.arange(0, dataset.shape[1]), [0]), + names=self.multi_index, + ) + series.append(Series(dataset[()].ravel(), index=index, name=channel)) + + return concat(series, axis=1) # much faster when concatenating similarly indexed data first + + @property + def df_train(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per train]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_train' channel's data. + """ + series = [] + + channels = get_channels(self._config["channels"], "per_train") + + for channel in channels: + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, [0], [0]), + names=self.multi_index, + ) + if channel == "dldAux": + aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() + for name, slice_aux in aux_channels: + series.append(Series(dataset[: key.size, slice_aux], index, name=name)) + else: + series.append(Series(dataset, index, name=channel)) + + return concat(series, axis=1) + + def validate_channel_keys(self) -> None: + """ + Validates if the index and dataset keys for all channels in config exist in the h5 file. + + Raises: + KeyError: If the index or dataset keys do not exist in the file. + """ + for channel in self._config["channels"]: + index_key, dataset_key = self.get_index_dataset_key(channel) + if index_key not in self.h5_file: + raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") + if dataset_key not in self.h5_file: + raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") + + @property + def df(self) -> DataFrame: + """ + Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, + returning a single dataframe. + + Returns: + DataFrame: The combined pandas DataFrame. + """ + + self.validate_channel_keys() + return ( + self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") + .join(self.df_train, on=self.multi_index, how="outer") + .sort_index() + ) + + +class BufferHandler: + """ + A class for handling the creation and manipulation of buffer files using DataFrameCreator + and ParquetHandler. + """ + + def __init__( + self, + cfg_df: dict, + h5_paths: list[Path], + folder: Path, + force_recreate: bool = False, + prefix: str = "", + suffix: str = "", + debug: bool = False, + auto: bool = True, + ) -> None: + """ + Initializes the BufferFileHandler. + + Args: + cfg_df (dict): The configuration dictionary with only the dataframe key. + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + force_recreate (bool): Flag to force recreation of buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + debug (bool): Flag to enable debug mode. + auto (bool): Flag to automatically create buffer files and fill the dataframe. + """ + self._config = cfg_df + + self.buffer_paths: list[Path] = [] + self.h5_to_create: list[Path] = [] + self.buffer_to_create: list[Path] = [] + + self.dataframe_electron: dd.DataFrame = None + self.dataframe_pulse: dd.DataFrame = None + + # In auto mode, these methods are called automatically + if auto: + self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + + if not force_recreate: + self.schema_check() + + self.create_buffer_files(debug) + + self.get_filled_dataframe() + + def schema_check(self) -> None: + """ + Checks the schema of the Parquet files. + + Raises: + ValueError: If the schema of the Parquet files does not match the configuration. + """ + existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] + parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] + config_schema = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + + for i, schema in enumerate(parquet_schemas): + schema_set = set(schema.names) + if schema_set != config_schema: + missing_in_parquet = config_schema - schema_set missing_in_config = schema_set - config_schema missing_in_parquet_str = ( @@ -602,284 +882,4 @@ def read_parquet(self) -> list[dd.DataFrame]: return dfs -class FlashLoader(BaseLoader): - """ - The class generates multiindexed multidimensional pandas dataframes from the new FLASH - dataformat resolved by both macro and microbunches alongside electrons. - Only the read_dataframe (inherited and implemented) method is accessed by other modules. - """ - - __name__ = "flash" - - supported_file_types = ["h5"] - - def __init__(self, config: dict) -> None: - """ - Initializes the FlashLoader. - - Args: - config (dict): Configuration dictionary. - """ - super().__init__(config=config) - - def initialize_paths(self) -> tuple[list[Path], Path]: - """ - Initializes the paths based on the configuration. - - Returns: - Tuple[List[Path], Path]: A tuple containing a list of raw data directories - paths and the parquet data directory path. - - Raises: - ValueError: If required values are missing from the configuration. - FileNotFoundError: If the raw data directories are not found. - """ - # Parses to locate the raw beamtime directory from config file - if "paths" in self._config["core"]: - data_raw_dir = [ - Path(self._config["core"]["paths"].get("data_raw_dir", "")), - ] - data_parquet_dir = Path( - self._config["core"]["paths"].get("data_parquet_dir", ""), - ) - - else: - try: - beamtime_id = self._config["core"]["beamtime_id"] - year = self._config["core"]["year"] - daq = self._config["dataframe"]["daq"] - except KeyError as exc: - raise ValueError( - "The beamtime_id, year and daq are required.", - ) from exc - - beamtime_dir = Path( - self._config["dataframe"]["beamtime_dir"][self._config["core"]["beamline"]], - ) - beamtime_dir = beamtime_dir.joinpath(f"{year}/data/{beamtime_id}/") - - # Use pathlib walk to reach the raw data directory - data_raw_dir = [] - raw_path = beamtime_dir.joinpath("raw") - - for path in raw_path.glob("**/*"): - if path.is_dir(): - dir_name = path.name - if dir_name.startswith("express-") or dir_name.startswith( - "online-", - ): - data_raw_dir.append(path.joinpath(daq)) - elif dir_name == daq.upper(): - data_raw_dir.append(path) - - if not data_raw_dir: - raise FileNotFoundError("Raw data directories not found.") - - parquet_path = "processed/parquet" - data_parquet_dir = beamtime_dir.joinpath(parquet_path) - - data_parquet_dir.mkdir(parents=True, exist_ok=True) - - return data_raw_dir, data_parquet_dir - - def get_files_from_run_id( - self, - run_id: str, - folders: str | Sequence[str] = None, - extension: str = "h5", - **kwds, - ) -> list[str]: - """ - Returns a list of filenames for a given run located in the specified directory - for the specified data acquisition (daq). - - Args: - run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw - data is located. Defaults to config["core"]["base_folder"]. - extension (str, optional): The file extension. Defaults to "h5". - kwds: Keyword arguments: - - daq (str): The data acquisition identifier. - - Returns: - List[str]: A list of path strings representing the collected file names. - - Raises: - FileNotFoundError: If no files are found for the given run in the directory. - """ - # Define the stream name prefixes based on the data acquisition identifier - stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] - - if folders is None: - folders = self._config["core"]["base_folder"] - - if isinstance(folders, str): - folders = [folders] - - daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) - - # Generate the file patterns to search for in the directory - file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension - - files: list[Path] = [] - # Use pathlib to search for matching files in each directory - for folder in folders: - files.extend( - natsorted( - Path(folder).glob(file_pattern), - key=lambda filename: str(filename).rsplit("_", maxsplit=1)[-1], - ), - ) - - # Check if any files are found - if not files: - raise FileNotFoundError( - f"No files found for run {run_id} in directory {str(folders)}", - ) - - # Return the list of found files - return [str(file.resolve()) for file in files] - - def parse_metadata(self, scicat_token: str = None, **kwds) -> dict: - """Uses the MetadataRetriever class to fetch metadata from scicat for each run. - - Returns: - dict: Metadata dictionary - scicat_token (str, optional):: The scicat token to use for fetching metadata - """ - metadata_retriever = MetadataRetriever(self._config["metadata"], scicat_token) - metadata = metadata_retriever.get_metadata( - beamtime_id=self._config["core"]["beamtime_id"], - runs=self.runs, - metadata=self.metadata, - ) - - return metadata - - def get_count_rate( - self, - fids: Sequence[int] = None, - **kwds, - ): - return None, None - - def get_elapsed_time(self, fids=None, **kwds): - return None - - def read_dataframe( - self, - files: str | Sequence[str] = None, - folders: str | Sequence[str] = None, - runs: str | Sequence[str] = None, - ftype: str = "h5", - metadata: dict = None, - collect_metadata: bool = False, - converted: bool = False, - load_parquet: bool = False, - save_parquet: bool = False, - detector: str = "", - force_recreate: bool = False, - parquet_dir: str | Path = None, - debug: bool = False, - **kwds, - ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: - """ - Read express data from the DAQ, generating a parquet in between. - - Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files are stored - Path has priority such that if it's specified, the specified files will be ignored. - Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding files will - be located in the location provided by ``folders``. Takes precedence over - ``files`` and ``folders``. Defaults to None. - ftype (str, optional): The file extension type. Defaults to "h5". - metadata (dict, optional): Additional metadata. Defaults to None. - collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. - - Returns: - Tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame - and metadata. - - Raises: - ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. - FileNotFoundError: If the conversion fails for some files or no data is available. - """ - t0 = time.time() - - data_raw_dir, data_parquet_dir = self.initialize_paths() - - # Prepare a list of names for the runs to read and parquets to write - if runs is not None: - files = [] - if isinstance(runs, (str, int)): - runs = [runs] - for run in runs: - run_files = self.get_files_from_run_id( - run_id=run, - folders=[str(folder.resolve()) for folder in data_raw_dir], - extension=ftype, - daq=self._config["dataframe"]["daq"], - ) - files.extend(run_files) - self.runs = list(runs) - super().read_dataframe(files=files, ftype=ftype) - - else: - # This call takes care of files and folders. As we have converted runs into files - # already, they are just stored in the class by this call. - super().read_dataframe( - files=files, - folders=folders, - ftype=ftype, - metadata=metadata, - ) - - # if parquet_dir is None, use data_parquet_dir - parquet_dir = parquet_dir or data_parquet_dir - parquet_path = Path(parquet_dir) - filename = "_".join(str(run) for run in self.runs) - converted_str = "converted" if converted else "" - # Create parquet paths for saving and loading the parquet files of df and timed_df - ph = ParquetHandler( - [filename, filename + "_timed"], - parquet_path, - converted_str, - "run_", - detector, - ) - - # Check if load_parquet is flagged and then load the file if it exists - if load_parquet: - df_list = ph.read_parquet() - df = df_list[0] - df_timed = df_list[1] - - # Default behavior is to create the buffer files and load them - else: - # Obtain the parquet filenames, metadata, and schema from the method - # which handles buffer file creation/reading - h5_paths = [Path(file) for file in self.files] - buffer = BufferHandler( - self._config["dataframe"], - h5_paths, - parquet_path, - force_recreate, - suffix=detector, - debug=debug, - ) - df = buffer.dataframe_electron - df_timed = buffer.dataframe_pulse - - # Save the dataframe as parquet if requested - if save_parquet: - ph.save_parquet([df, df_timed], drop_index=True) - - metadata = self.parse_metadata(**kwds) if collect_metadata else {} - print(f"loading complete in {time.time() - t0: .2f} s") - - return df, df_timed, metadata - - LOADER = FlashLoader From 74b41dca5b9a017cb5d2a1c97c77645f910e6b30 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 27 Mar 2024 23:04:16 +0100 Subject: [PATCH 037/300] update interface from suggestions --- sed/loader/flash/loader.py | 116 +++++++++++---------- tests/loader/flash/test_buffer_handler.py | 38 +++---- tests/loader/flash/test_parquet_handler.py | 9 +- 3 files changed, 87 insertions(+), 76 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 9f6300d4..f577b13d 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -275,11 +275,11 @@ def read_dataframe( converted_str = "converted" if converted else "" # Create parquet paths for saving and loading the parquet files of df and timed_df ph = ParquetHandler( - [filename, filename + "_timed"], - parquet_path, - converted_str, - "run_", - detector, + parquet_names=[filename, filename + "_timed"], + folder=parquet_path, + subfolder=converted_str, + prefix="run_", + suffix=detector, ) # Check if load_parquet is flagged and then load the file if it exists @@ -294,10 +294,12 @@ def read_dataframe( # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] buffer = BufferHandler( - self._config["dataframe"], - h5_paths, - parquet_path, - force_recreate, + config_dataframe=self._config["dataframe"], + ) + buffer.run( + h5_paths=h5_paths, + folder=parquet_path, + force_recreate=force_recreate, suffix=detector, debug=debug, ) @@ -319,18 +321,18 @@ class DataFrameCreator: Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ - def __init__(self, cfg_df: dict, h5_file: h5py.File) -> None: + def __init__(self, config_dataframe: dict, h5_file: h5py.File) -> None: """ Initializes the DataFrameCreator class. Args: - cfg_df (dict): The configuration dictionary with only the dataframe key. + config_dataframe (dict): The configuration dictionary with only the dataframe key. h5_file (h5py.File): The open h5 file. """ self.h5_file: h5py.File = h5_file self.failed_files_error: list[str] = [] self.multi_index = get_channels(index=True) - self._config = cfg_df + self._config = config_dataframe def get_index_dataset_key(self, channel: str) -> tuple[str, str]: """ @@ -574,48 +576,29 @@ class BufferHandler: def __init__( self, - cfg_df: dict, - h5_paths: list[Path], - folder: Path, - force_recreate: bool = False, - prefix: str = "", - suffix: str = "", - debug: bool = False, - auto: bool = True, + config_dataframe: dict, ) -> None: """ Initializes the BufferFileHandler. Args: - cfg_df (dict): The configuration dictionary with only the dataframe key. + config_dataframe (dict): The configuration dictionary with only the dataframe key. h5_paths (List[Path]): List of paths to H5 files. folder (Path): Path to the folder for buffer files. force_recreate (bool): Flag to force recreation of buffer files. prefix (str): Prefix for buffer file names. suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode. - auto (bool): Flag to automatically create buffer files and fill the dataframe. """ - self._config = cfg_df + self._config = config_dataframe self.buffer_paths: list[Path] = [] - self.h5_to_create: list[Path] = [] - self.buffer_to_create: list[Path] = [] + self.missing_h5_files: list[Path] = [] + self.save_paths: list[Path] = [] self.dataframe_electron: dd.DataFrame = None self.dataframe_pulse: dd.DataFrame = None - # In auto mode, these methods are called automatically - if auto: - self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) - - if not force_recreate: - self.schema_check() - - self.create_buffer_files(debug) - - self.get_filled_dataframe() - def schema_check(self) -> None: """ Checks the schema of the Parquet files. @@ -684,10 +667,10 @@ def get_files_to_read( ] # Get the list of H5 files to read and the corresponding buffer files to create - self.h5_to_create = list(compress(h5_paths, files_to_read)) - self.buffer_to_create = list(compress(self.buffer_paths, files_to_read)) + self.missing_h5_files = list(compress(h5_paths, files_to_read)) + self.save_paths = list(compress(self.buffer_paths, files_to_read)) - self.num_files = len(self.h5_to_create) + self.num_files = len(self.missing_h5_files) print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") @@ -703,7 +686,7 @@ def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: h5_file = h5py.File(h5_path, "r") # Create a DataFrameCreator instance with the configuration and the h5 file - dfc = DataFrameCreator(self._config, h5_file) + dfc = DataFrameCreator(config_dataframe=self._config, h5_file=h5_file) # Get the DataFrame from the DataFrameCreator instance df = dfc.df @@ -723,15 +706,15 @@ def create_buffer_files(self, debug: bool) -> None: """ if self.num_files > 0: if debug: - for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create): + for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): self._create_buffer_file(h5_path, parquet_path) else: Parallel(n_jobs=self.num_files, verbose=10)( delayed(self._create_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in zip(self.h5_to_create, self.buffer_to_create) + for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) ) - def get_filled_dataframe(self) -> None: + def fill_dataframes(self) -> None: """ Reads all parquet files into one dataframe using dask and fills NaN values. """ @@ -776,6 +759,36 @@ def get_filled_dataframe(self) -> None: self.dataframe_electron = dataframe_electron.astype(dtypes) self.dataframe_pulse = dataframe[index + channels] + def run( + self, + h5_paths: list[Path], + folder: Path, + force_recreate: bool = False, + prefix: str = "", + suffix: str = "", + debug: bool = False, + ) -> None: + """ + Runs the buffer file creation process. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + force_recreate (bool): Flag to force recreation of buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + debug (bool): Flag to enable debug mode.): + """ + + self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + + if not force_recreate: + self.schema_check() + + self.create_buffer_files(debug) + + self.fill_dataframes() + class ParquetHandler: """A class for handling the creation and manipulation of Parquet files.""" @@ -788,7 +801,7 @@ def __init__( prefix: str = "", suffix: str = "", extension: str = "parquet", - parquet_paths: Path = None, + parquet_paths: list[Path] = None, ): """ A handler for saving and reading Dask DataFrames to/from Parquet files. @@ -799,7 +812,7 @@ def __init__( subfolder (str): Optional subfolder within the main folder. prefix (str): Optional prefix for the Parquet file name. suffix (str): Optional suffix for the Parquet file name. - parquet_path (Path): Optional custom path for the Parquet file. + parquet_paths (List[Path]): Optional custom path for the Parquet file. """ self.parquet_paths: list[Path] = None @@ -815,9 +828,7 @@ def __init__( # If parquet_paths is provided, use it and ignore the other arguments # Else, initialize the paths if parquet_paths: - self.parquet_paths = ( - parquet_paths if isinstance(parquet_paths, list) else [parquet_paths] - ) + self.parquet_paths = parquet_paths else: self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) @@ -846,24 +857,23 @@ def _initialize_paths( def save_parquet( self, - dfs: dd.DataFrame | list[dd.DataFrame], + dfs: list[dd.DataFrame], drop_index: bool = False, ) -> None: """ - Save the DataFrame to a Parquet file. + Saves the DataFrames to Parquet files. Args: - dfs (DataFrame | dd.DataFrame): The pandas or Dask Dataframe to be saved. + dfs (dd.DataFrame): The Dask Dataframes to be saved. drop_index (bool): If True, drops the index before saving. """ # Compute the Dask DataFrame, reset the index, and save to Parquet - dfs = dfs if isinstance(dfs, list) else [dfs] for df, parquet_path in zip(dfs, self.parquet_paths): df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) def read_parquet(self) -> list[dd.DataFrame]: """ - Read a Dask DataFrame from the Parquet file. + Creates a list of Dask DataFrames from a list of Parquet files. Returns: dd.DataFrame: The Dask DataFrame read from the Parquet file. diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index fbe60b05..4b62c553 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -19,18 +19,18 @@ def test_get_files_to_read(config, h5_paths): folder = create_parquet_dir(config, "get_files_to_read") subfolder = folder.joinpath("buffer") # set to false to avoid creating buffer files unnecessarily - bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=False) + bh = BufferHandler(config["dataframe"]) bh.get_files_to_read(h5_paths, folder, "", "", False) assert bh.num_files == len(h5_paths) - assert len(bh.buffer_to_create) == len(h5_paths) + assert len(bh.save_paths) == len(h5_paths) - assert np.all(bh.h5_to_create == h5_paths) + assert np.all(bh.missing_h5_files == h5_paths) # create expected paths expected_buffer_paths = [Path(subfolder, f"{Path(path).stem}") for path in h5_paths] - assert np.all(bh.buffer_to_create == expected_buffer_paths) + assert np.all(bh.save_paths == expected_buffer_paths) # create only one buffer file bh._create_buffer_file(h5_paths[0], expected_buffer_paths[0]) @@ -47,7 +47,7 @@ def test_get_files_to_read(config, h5_paths): expected_buffer_paths = [ Path(subfolder, f"prefix_{Path(path).stem}_suffix") for path in h5_paths ] - assert np.all(bh.buffer_to_create == expected_buffer_paths) + assert np.all(bh.save_paths == expected_buffer_paths) def test_buffer_schema_mismatch(config, h5_paths): @@ -67,7 +67,8 @@ def test_buffer_schema_mismatch(config, h5_paths): - Clean up created buffer files after the test. """ folder = create_parquet_dir(config, "schema_mismatch") - bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths=h5_paths, folder=folder, debug=True) # Manipulate the configuration to introduce a new channel 'gmdTunnel2' config_dict = config @@ -79,7 +80,8 @@ def test_buffer_schema_mismatch(config, h5_paths): # Reread the dataframe with the modified configuration, expecting a schema mismatch error with pytest.raises(ValueError) as e: - bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args # Validate the specific error messages for schema mismatch @@ -88,21 +90,16 @@ def test_buffer_schema_mismatch(config, h5_paths): assert expected_error[4] == "Please check the configuration file or set force_recreate to True." # Force recreation of the dataframe, including the added channel 'gmdTunnel2' - bh = BufferHandler( - config["dataframe"], - h5_paths, - folder, - auto=True, - force_recreate=True, - debug=True, - ) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths=h5_paths, folder=folder, force_recreate=True, debug=True) # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario del config["dataframe"]["channels"]["gmdTunnel2"] # also results in error but different from before with pytest.raises(ValueError) as e: # Attempt to read the dataframe again to check for the missing channel error - bh = BufferHandler(config["dataframe"], h5_paths, folder, auto=True, debug=True) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args # Check for the specific error message indicating a missing channel in the configuration @@ -114,10 +111,12 @@ def test_buffer_schema_mismatch(config, h5_paths): def test_create_buffer_files(config, h5_paths): folder_serial = create_parquet_dir(config, "create_buffer_files_serial") - bh_serial = BufferHandler(config["dataframe"], h5_paths, folder_serial, debug=True) + bh_serial = BufferHandler(config["dataframe"]) + bh_serial.run(h5_paths, folder_serial, debug=True) folder_parallel = create_parquet_dir(config, "create_buffer_files_parallel") - bh_parallel = BufferHandler(config["dataframe"], h5_paths, folder_parallel) + bh_parallel = BufferHandler(config["dataframe"]) + bh_parallel.run(h5_paths, folder_parallel) df_serial = pd.read_parquet(folder_serial) df_parallel = pd.read_parquet(folder_parallel) @@ -132,7 +131,8 @@ def test_create_buffer_files(config, h5_paths): def test_get_filled_dataframe(config, h5_paths): """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") - bh = BufferHandler(config["dataframe"], h5_paths, folder) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths, folder) df = pd.read_parquet(folder) diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index 76338c86..6ac008d0 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -51,12 +51,13 @@ def test_save_read_parquet(config, h5_paths): folder = create_parquet_dir(config, "parquet_save") parquet_path = folder.joinpath("test.parquet") - ph = ParquetHandler(parquet_paths=parquet_path) + ph = ParquetHandler(parquet_paths=[parquet_path]) print(ph.parquet_paths) - bh = BufferHandler(config["dataframe"], h5_paths, folder) - ph.save_parquet(bh.dataframe_electron, drop_index=True) + bh = BufferHandler(config["dataframe"]) + bh.run(h5_paths=h5_paths, folder=folder) + ph.save_parquet([bh.dataframe_electron], drop_index=True) parquet_path.unlink() - ph.save_parquet(bh.dataframe_electron, drop_index=False) + ph.save_parquet([bh.dataframe_electron], drop_index=False) df = ph.read_parquet() From b937db88fff4a228ab85a421b40cfb9bed14b3d0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 27 Mar 2024 23:14:40 +0100 Subject: [PATCH 038/300] limit the cores used --- sed/loader/flash/loader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index f577b13d..141bfc54 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -9,6 +9,7 @@ """ from __future__ import annotations +import os import time from itertools import compress from pathlib import Path @@ -704,12 +705,15 @@ def create_buffer_files(self, debug: bool) -> None: Args: debug (bool): Flag to enable debug mode, which serializes the creation. """ - if self.num_files > 0: + # make sure to not create more jobs than cores available + # TODO: This value should be taken from the configuration + n_cores = min(self.num_files, os.cpu_count() - 1) + if n_cores > 0: if debug: for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): self._create_buffer_file(h5_path, parquet_path) else: - Parallel(n_jobs=self.num_files, verbose=10)( + Parallel(n_jobs=n_cores, verbose=10)( delayed(self._create_buffer_file)(h5_path, parquet_path) for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) ) From 9dc69aa21510d7b3de18e80666a72d93c765f5c0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 28 Mar 2024 00:25:57 +0100 Subject: [PATCH 039/300] change interface of parquethandler to suggested --- sed/loader/flash/loader.py | 116 ++++++++++++--------- tests/loader/flash/test_parquet_handler.py | 41 ++++++-- 2 files changed, 101 insertions(+), 56 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 141bfc54..74ebeb73 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -653,15 +653,16 @@ def get_files_to_read( force_recreate (bool): Flag to force recreation of buffer files. """ # Getting the paths of the buffer files, with subfolder as buffer and no extension - pq_handler = ParquetHandler( - [Path(h5_path).stem for h5_path in h5_paths], - folder, - "buffer", - prefix, - suffix, + ph = ParquetHandler( + parquet_names=[Path(h5_path).stem for h5_path in h5_paths], + folder=folder, + subfolder="buffer", + prefix=prefix, + suffix=suffix, extension="", ) - self.buffer_paths = pq_handler.parquet_paths + ph.initialize_paths() + self.buffer_paths = ph.parquet_paths # read only the files that do not exist or if force_recreate is True files_to_read = [ force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths @@ -746,12 +747,12 @@ def fill_dataframes(self) -> None: dataframe_electron = dataframe.dropna(subset=tof_column) # Set the dtypes of the channels here as there should be no null values - ch_dtypes = get_channels(self._config["channels"], "all") - cfg_ch = self._config["channels"] + channel_dtypes = get_channels(self._config["channels"], "all") + config_channels = self._config["channels"] dtypes = { - channel: cfg_ch[channel].get("dtype") - for channel in ch_dtypes - if cfg_ch[channel].get("dtype") is not None + channel: config_channels[channel].get("dtype") + for channel in channel_dtypes + if config_channels[channel].get("dtype") is not None } # Correct the 3-bit shift which encodes the detector ID in the 8s time @@ -805,10 +806,9 @@ def __init__( prefix: str = "", suffix: str = "", extension: str = "parquet", - parquet_paths: list[Path] = None, ): """ - A handler for saving and reading Dask DataFrames to/from Parquet files. + Initialize the ParquetHandler. Args: parquet_names Union[str, List[str]]: The base name of the Parquet files. @@ -816,75 +816,97 @@ def __init__( subfolder (str): Optional subfolder within the main folder. prefix (str): Optional prefix for the Parquet file name. suffix (str): Optional suffix for the Parquet file name. - parquet_paths (List[Path]): Optional custom path for the Parquet file. + extension (str): Optional extension for the Parquet file names. """ - self.parquet_paths: list[Path] = None if isinstance(parquet_names, str): parquet_names = [parquet_names] - if not folder and not parquet_paths: - raise ValueError("Please provide folder or parquet_paths.") - if folder and not parquet_names: - raise ValueError("With folder, please provide parquet_names.") - - # If parquet_paths is provided, use it and ignore the other arguments - # Else, initialize the paths - if parquet_paths: - self.parquet_paths = parquet_paths - else: - self._initialize_paths(parquet_names, folder, subfolder, prefix, suffix, extension) + self.parquet_names = parquet_names + self.folder = folder + self.subfolder = subfolder + self.prefix = prefix + self.suffix = suffix + self.extension = extension - def _initialize_paths( + def initialize_paths( self, - parquet_names: list[str], - folder: Path, - subfolder: str = None, - prefix: str = None, - suffix: str = None, - extension: str = None, + parquet_paths: list[Path] = None, ) -> None: """ - Create the directory for the Parquet file. + Initialize the paths for the Parquet files. + + If custom paths are provided, they will be used. Otherwise, paths will be generated based on + the specified parameters during initialization. + + Args: + parquet_paths (List[Path]): Optional custom paths for the Parquet files. """ - # Create the full path for the Parquet file - parquet_dir = folder.joinpath(subfolder) + # If parquet_paths is provided, use it and return + if parquet_paths: + self.parquet_paths = parquet_paths + return + + if self.parquet_paths: + return + + if not self.folder and not parquet_paths: + raise ValueError("Please provide folder or parquet_paths.") + if self.folder and not self.parquet_names: + raise ValueError("With folder, please provide parquet_names.") + + # Otherwise create the full path for the Parquet file + parquet_dir = self.folder.joinpath(self.subfolder) parquet_dir.mkdir(parents=True, exist_ok=True) - if extension: - extension = f".{extension}" # to be backwards compatible + if self.extension: + self.extension = f".{self.extension}" # to be backwards compatible self.parquet_paths = [ - parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) - for name in parquet_names + parquet_dir.joinpath(Path(f"{self.prefix}{name}{self.suffix}{self.extension}")) + for name in self.parquet_names ] def save_parquet( self, dfs: list[dd.DataFrame], + parquet_paths: list[Path] = None, drop_index: bool = False, ) -> None: """ - Saves the DataFrames to Parquet files. + Save the Dask DataFrames to Parquet files. Args: - dfs (dd.DataFrame): The Dask Dataframes to be saved. + dfs (list[dd.DataFrame]): The list of Dask DataFrames to be saved. + parquet_paths (List[Path]): Optional custom paths for the Parquet files. drop_index (bool): If True, drops the index before saving. """ + self.initialize_paths(parquet_paths) + + if len(dfs) != len(self.parquet_paths): + raise ValueError("Number of DataFrames provided does not match the number of paths.") + # Compute the Dask DataFrame, reset the index, and save to Parquet for df, parquet_path in zip(dfs, self.parquet_paths): df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) - def read_parquet(self) -> list[dd.DataFrame]: + def read_parquet( + self, + parquet_paths: list[Path] = None, + ) -> list[dd.DataFrame]: """ - Creates a list of Dask DataFrames from a list of Parquet files. + Read Dask DataFrames from Parquet files. + + Args: + parquet_paths (List[Path]): Optional custom paths for the Parquet files. Returns: - dd.DataFrame: The Dask DataFrame read from the Parquet file. + List[dd.DataFrame]: The list of Dask DataFrames read from the Parquet files. Raises: - FileNotFoundError: If the Parquet file does not exist. + FileNotFoundError: If any of the specified Parquet files do not exist. """ + self.initialize_paths(parquet_paths) dfs = [] for parquet_path in self.parquet_paths: if not parquet_path.exists(): diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index 6ac008d0..e241d491 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -1,5 +1,6 @@ from pathlib import Path +import numpy as np import pytest from sed.loader.flash.loader import BufferHandler @@ -16,12 +17,14 @@ def create_parquet_dir(config, folder): def test_parquet_init_error(): """Test ParquetHandler initialization error""" with pytest.raises(ValueError) as e: - ParquetHandler(parquet_names="test") + ph = ParquetHandler(parquet_names="test") + ph.initialize_paths() assert "Please provide folder or parquet_paths." in str(e.value) with pytest.raises(ValueError) as e: - ParquetHandler(folder="test") + ph = ParquetHandler(folder="test") + ph.initialize_paths() assert "With folder, please provide parquet_names." in str(e.value) @@ -31,15 +34,18 @@ def test_initialize_paths(config): folder = create_parquet_dir(config, "parquet_init") ph = ParquetHandler("test", folder, extension="xyz") + ph.initialize_paths() assert ph.parquet_paths[0].suffix == ".xyz" assert ph.parquet_paths[0].name == "test.xyz" # test prefix and suffix ph = ParquetHandler("test", folder, prefix="prefix_", suffix="_suffix") + ph.initialize_paths() assert ph.parquet_paths[0].name == "prefix_test_suffix.parquet" # test with list of parquet_names and subfolder ph = ParquetHandler(["test1", "test2"], folder, subfolder="subfolder") + ph.initialize_paths() assert ph.parquet_paths[0].parent.name == "subfolder" assert ph.parquet_paths[0].name == "test1.parquet" assert ph.parquet_paths[1].name == "test2.parquet" @@ -51,18 +57,35 @@ def test_save_read_parquet(config, h5_paths): folder = create_parquet_dir(config, "parquet_save") parquet_path = folder.joinpath("test.parquet") - ph = ParquetHandler(parquet_paths=[parquet_path]) - print(ph.parquet_paths) + # create some parquet files for testing save and read + ph = ParquetHandler() bh = BufferHandler(config["dataframe"]) bh.run(h5_paths=h5_paths, folder=folder) - ph.save_parquet([bh.dataframe_electron], drop_index=True) - parquet_path.unlink() - ph.save_parquet([bh.dataframe_electron], drop_index=False) + + ph.save_parquet([bh.dataframe_electron], parquet_paths=[parquet_path], drop_index=False) + parquet_path.unlink() # remove parquet file + ph.save_parquet([bh.dataframe_electron], parquet_paths=[parquet_path], drop_index=True) + + # Provide different number of DataFrames and paths + with pytest.raises(ValueError) as e: + ph.save_parquet( + [bh.dataframe_electron], + parquet_paths=[parquet_path, parquet_path], + drop_index=False, + ) + + assert "Number of DataFrames provided does not match the number of paths." in str(e.value) df = ph.read_parquet() + df_loaded = df[0].compute() + df_saved = bh.dataframe_electron.compute().reset_index(drop=True) + + # compare the saved and loaded dataframes + assert np.all(df[0].columns == bh.dataframe_electron.columns) + assert df_loaded.equals(df_saved) - [path.unlink() for path in bh.buffer_paths] - parquet_path.unlink() + [path.unlink() for path in bh.buffer_paths] # remove buffer files + parquet_path.unlink() # remove parquet file # Test file not found with pytest.raises(FileNotFoundError) as e: ph.read_parquet() From 09a93d3c20c699c23a6f8a017198bef2b1cfa06f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 28 Mar 2024 11:36:30 +0100 Subject: [PATCH 040/300] fix bug for df indexing --- sed/loader/flash/loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 74ebeb73..606a6d07 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -484,7 +484,8 @@ def df_electron(self) -> DataFrame: # MultiIndex is set # Finally, the offset values are dropped return ( - dataframe.dropna()[indexer] + dataframe.dropna() + .iloc[indexer] .set_index(index) .drop(index=drop_vals, level="pulseId", errors="ignore") ) From 56e4788dc956c8c4d195b161815c47b27055edbe Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 11:54:10 +0200 Subject: [PATCH 041/300] change version to 3.9 --- .github/workflows/benchmark.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/linting.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/testing_coverage.yml | 2 +- .github/workflows/testing_multiversion.yml | 2 +- .github/workflows/update_dependencies.yml | 2 +- pyproject.toml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cfd9a6e3..89e0d708 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -28,7 +28,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: packetcoders/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 # Run benchmakrs diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 35784a76..5b191284 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -47,7 +47,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: packetcoders/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 - name: Install notebook dependencies diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index e750911c..f12a5f1e 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -19,7 +19,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: packetcoders/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 # Linting steps, excute all linters even if one fails diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b90abfb6..75996bee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: zain-sohail/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 working-directory: sed-processor @@ -82,7 +82,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: zain-sohail/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 working-directory: sed-processor diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index 96e9d1e7..515042ab 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -23,7 +23,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: packetcoders/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 # Run pytest with coverage report, saving to xml diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index c3ef192c..d8ef0e9b 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -12,7 +12,7 @@ jobs: # Using matrix strategy strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest steps: # Check out repo and set up Python diff --git a/.github/workflows/update_dependencies.yml b/.github/workflows/update_dependencies.yml index 78b60002..166378e0 100644 --- a/.github/workflows/update_dependencies.yml +++ b/.github/workflows/update_dependencies.yml @@ -28,7 +28,7 @@ jobs: - name: "Setup Python, Poetry and Dependencies" uses: packetcoders/action-setup-cache-python-poetry@main with: - python-version: 3.8 + python-version: 3.9 poetry-version: 1.2.2 # update poetry lockfile diff --git a/pyproject.toml b/pyproject.toml index 52e4b57c..39b8279a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ keywords = ["sed", "mpes", "flash", "arpes"] license = "MIT" [tool.poetry.dependencies] -python = ">=3.8, <3.12" +python = ">=3.9, <3.13" bokeh = ">=2.4.2" dask = ">=2021.12.0" fastdtw = ">=0.3.4" From 3d5ffb483180cebce75d801ee5c32067b738fb00 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 12:09:31 +0200 Subject: [PATCH 042/300] update lock file --- .github/workflows/testing_multiversion.yml | 3 +- poetry.lock | 1666 +++++++++----------- 2 files changed, 740 insertions(+), 929 deletions(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index d8ef0e9b..896368a4 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -1,4 +1,5 @@ -name: unit tests [Python 3.8|3.9|3.10|3.11] +# Tests for all supported versions [Python 3.9|3.10|3.11|3.12] +name: Unit Tests on: workflow_dispatch: diff --git a/poetry.lock b/poetry.lock index c300c6f7..f212450d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -31,13 +31,13 @@ docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -197,56 +197,70 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "astropy" -version = "5.2.2" +version = "6.0.1" description = "Astronomy and astrophysics core library" optional = false +python-versions = ">=3.9" +files = [ + {file = "astropy-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445"}, + {file = "astropy-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a"}, + {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466"}, + {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88"}, + {file = "astropy-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1"}, + {file = "astropy-6.0.1-cp310-cp310-win32.whl", hash = "sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1"}, + {file = "astropy-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c"}, + {file = "astropy-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23"}, + {file = "astropy-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292"}, + {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e"}, + {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363"}, + {file = "astropy-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4"}, + {file = "astropy-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5"}, + {file = "astropy-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27"}, + {file = "astropy-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97"}, + {file = "astropy-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f"}, + {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc"}, + {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165"}, + {file = "astropy-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4"}, + {file = "astropy-6.0.1-cp312-cp312-win32.whl", hash = "sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723"}, + {file = "astropy-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3"}, + {file = "astropy-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9"}, + {file = "astropy-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6"}, + {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08"}, + {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf"}, + {file = "astropy-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0"}, + {file = "astropy-6.0.1-cp39-cp39-win32.whl", hash = "sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563"}, + {file = "astropy-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23"}, + {file = "astropy-6.0.1.tar.gz", hash = "sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c"}, +] + +[package.dependencies] +astropy-iers-data = ">=0.2024.2.26.0.28.55" +numpy = ">=1.22,<2" +packaging = ">=19.0" +pyerfa = ">=2.0.1.1" +PyYAML = ">=3.13" + +[package.extras] +all = ["asdf-astropy (>=0.3)", "astropy[recommended]", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2023.4.0)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2023.4.0)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] +docs = ["Jinja2 (>=3.1.3)", "astropy[recommended]", "pytest (>=7.0)", "sphinx", "sphinx-astropy[confv2] (>=1.9.1)", "sphinx-changelog (>=1.2.0)", "sphinx-design", "tomli"] +recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] +test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"] +test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"] + +[[package]] +name = "astropy-iers-data" +version = "0.2024.4.8.0.32.4" +description = "IERS Earth Rotation and Leap Second tables for the astropy core package" +optional = false python-versions = ">=3.8" files = [ - {file = "astropy-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66522e897daf3766775c00ef5c63b69beb0eb359e1f45d18745d0f0ca7f29cc1"}, - {file = "astropy-5.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ccf6f16cf7e520247ecc9d1a66dd4c3927fd60622203bdd1d06655ad81fa18f"}, - {file = "astropy-5.2.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3d0c37da922cdcb81e74437118fabd64171cbfefa06c7ea697a270e82a8164f2"}, - {file = "astropy-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04464e664a22382626ce9750ebe943b80a718dc8347134b9d138b63a2029f67a"}, - {file = "astropy-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f60cea0fa7cb6ebbd90373e48c07f5d459e95dfd6363f50e316e2db7755bead"}, - {file = "astropy-5.2.2-cp310-cp310-win32.whl", hash = "sha256:6c3abb2fa8ebaaad77875a02e664c1011f35bd0c0ef7d35a39b03c859de1129a"}, - {file = "astropy-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:185ade8c33cea34ba791b282e937686d98b4e205d4f343e686a4666efab2f6e7"}, - {file = "astropy-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f61c612e90e3dd3c075e99a61dedd53331c4577016c1d571aab00b95ca1731ab"}, - {file = "astropy-5.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3881e933ea870a27e5d6896443401fbf51e3b7e57c6356f333553f5ff0070c72"}, - {file = "astropy-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f210b5b4062030388437b9aca4bbf68f9063b2b27184006814a09fab41ac270e"}, - {file = "astropy-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e14b5a22f24ae5cf0404f21a4de135e26ca3c9cf55aefc5b0264a9ce24b53b0b"}, - {file = "astropy-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6768b3a670cdfff6c2416b3d7d1e4231839608299b32367e8b095959fc6733a6"}, - {file = "astropy-5.2.2-cp311-cp311-win32.whl", hash = "sha256:0aad85604cad40189b13d66bb46fb2a95df1a9095992071b31c3fa35b476fdbc"}, - {file = "astropy-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ac944158794a88789a007892ad91db35da14f689da1ab37c33c8de770a27f717"}, - {file = "astropy-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6703860deecd384bba2d2e338f77a0e7b46672812d27ed15f95e8faaa89fcd35"}, - {file = "astropy-5.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:124ef2a9f9b1cdbc1a5d514f7e57538253bb67ad031215f5f5405fc4cd31a4cd"}, - {file = "astropy-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:800501cc626aef0780dfb66156619699e98cb48854ed710f1ae3708aaab79f6e"}, - {file = "astropy-5.2.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22396592aa9b1653d37d552d3c52a8bb27ef072d077fad43b64faf841b1dcbf3"}, - {file = "astropy-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093782b1f0177c3dd2c04181ec016d8e569bd9e862b48236e40b14e2a7399170"}, - {file = "astropy-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c664f9194a4a3cece6215f651a9bc22c3cbd1f52dd450bd4d94eaf36f13c06c"}, - {file = "astropy-5.2.2-cp38-cp38-win32.whl", hash = "sha256:35ce00bb3dbc8bf7c842a0635354a5023cb64ae9c1925aa9b54629cf7fed2abe"}, - {file = "astropy-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:8304b590b20f9c161db85d5eb65d4c6323b3370a17c96ae163b18a0071cbd68a"}, - {file = "astropy-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:855748c2f1aedee5d770dfec8334109f1bcd1c1cee97f5915d3e888f43c04acf"}, - {file = "astropy-5.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ef9acc55c5fd70c7c78370389e79fb044321e531ac1facb7bddeef89d3132e3"}, - {file = "astropy-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f30b5d153b9d119783b96b948a3e0c4eb668820c06d2e8ba72f6ea989e4af5c1"}, - {file = "astropy-5.2.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530e6911a54a42e9f15b1a75dc3c699be3946c0b6ffdcfdcf4e14ae5fcfcd236"}, - {file = "astropy-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae3b383ac84fe6765e275f897f4010cc6afe6933607b7468561414dffdc4d915"}, - {file = "astropy-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b00a4cd49f8264a338b0020717bff104fbcca800bd50bf0a415d952078258a39"}, - {file = "astropy-5.2.2-cp39-cp39-win32.whl", hash = "sha256:b7167b9965ebd78b7c9da7e98a943381b25e23d041bd304ec2e35e8ec811cefc"}, - {file = "astropy-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:df81b8f23c5e906d799b47d2d8462707c745df38cafae0cd6674ef09e9a41789"}, - {file = "astropy-5.2.2.tar.gz", hash = "sha256:e6a9e34716bda5945788353c63f0644721ee7e5447d16b1cdcb58c48a96b0d9c"}, + {file = "astropy-iers-data-0.2024.4.8.0.32.4.tar.gz", hash = "sha256:420afca681a63ef2fa9fa22f78b7629008bd8638ed0a73f9c9288a08dca08bc7"}, + {file = "astropy_iers_data-0.2024.4.8.0.32.4-py3-none-any.whl", hash = "sha256:4d461830f74b7b8bedac00d45d1a5413b6db58442e2df7c38c6fdc7cabc7a553"}, ] -[package.dependencies] -numpy = ">=1.20" -packaging = ">=19.0" -pyerfa = ">=2.0" -PyYAML = ">=3.13" - [package.extras] -all = ["asdf (>=2.10.0)", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2022.8.2)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "mpmath", "pandas", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2022.8.2)", "scipy (>=1.5)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] -docs = ["Jinja2 (>=3.0)", "matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "pytest (>=7.0)", "scipy (>=1.3)", "sphinx", "sphinx-astropy (>=1.6)", "sphinx-changelog (>=1.2.0)"] -recommended = ["matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] -test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist"] -test-all = ["coverage[toml]", "ipython (>=4.2)", "objgraph", "pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "sgp4 (>=2.3)", "skyfield (>=1.20)"] +docs = ["pytest"] +test = ["hypothesis", "pytest", "pytest-remotedata"] [[package]] name = "asttokens" @@ -296,51 +310,9 @@ files = [ {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -optional = false -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -382,57 +354,68 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "blosc2" -version = "2.0.0" -description = "Python wrapper for the C-Blosc2 library." -optional = false -python-versions = ">=3.8, <4" -files = [ - {file = "blosc2-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4085e5c1df186e1747d8a8578b0cc1c8b7668391d635e9f89e17156912fba85a"}, - {file = "blosc2-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cec4c0570543921ce6b8c9ffdbf9f2170de37ecaf8e2b213e867e3130b81f205"}, - {file = "blosc2-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fbecd7ef4876811719aa58d84e4b414f430f329162c18578b870f5e77b59864"}, - {file = "blosc2-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96dd63eb7641594208e6a865fd60a0bdde24568a180180beb8af4d6608796f3a"}, - {file = "blosc2-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:659e18d5e606a0ca4d80766f86d87e640818051911d01679eed11022243a7e4f"}, - {file = "blosc2-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c58990ab2bcd0f412496acf1d05c65d955d963197bbaa57b10b2ace31c29181a"}, - {file = "blosc2-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f206e6a01a8167b441bf886ff022eb20e0f085b09300f49f3018f566c14d918a"}, - {file = "blosc2-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec63269428aa3fb45f7d4881b2d11b428c4cb62e854caf54a767a64da4df83e"}, - {file = "blosc2-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98ab1cd57f9d7422f1636a6b290f2940113ee8be26bfe3823e8c011826972b9c"}, - {file = "blosc2-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:54c5614b18f9f01473758fa64e3bc699adbe31b307a45eca0e07fa2204e4d4a1"}, - {file = "blosc2-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f465b8ab54ecde6b8654672a50a4c3699aafd8e2de0dfcd84ed53f8a34c1734a"}, - {file = "blosc2-2.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0497793f55db0b75de08eb4c047a0bc5b96dbe5e405b53803dd3368e36336188"}, - {file = "blosc2-2.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5c52649837d514669107c77e8f172e9e5ecfa030eef0d378bb47ce1689921c9"}, - {file = "blosc2-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb02f67d4ed8ac8f0ce5f3c8cafc0059255bb6899fd35127e4076925640f239"}, - {file = "blosc2-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:32c365bf744353103ed91dc1f03889de03b986588181601594aa7ee773818cb4"}, - {file = "blosc2-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d98e850f0de55e15c402c6e27105ba850f8954e784e30a7f8bde89eb70a08574"}, - {file = "blosc2-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6545d6d7e7365a2a3533d4bdf7095856443aed7d5ddc577ecd0e78083790bff1"}, - {file = "blosc2-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c193959a984e7814833c8be6b9026d7744d2cff4d450476561583a87152e13e"}, - {file = "blosc2-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea3396de7757092d502fb297a44a8d019d92e750e5aebcd9d39a157fde8785b3"}, - {file = "blosc2-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef018926b55799cf23345127dde8f29356b4451b3e067e1b07f0d186213bd821"}, - {file = "blosc2-2.0.0.tar.gz", hash = "sha256:f19b0b3674f6c825b490f00d8264b0c540c2cdc11ec7e81178d38b83c57790a1"}, +version = "2.5.1" +description = "Python wrapper for the C-Blosc2 library" +optional = false +python-versions = "<4,>=3.8" +files = [ + {file = "blosc2-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c861262b7fe317c1614a9b59b6c9edf409532b4a6aaf5b2f4ad0d79c6f800b57"}, + {file = "blosc2-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f35b5d69a7a41e9d5054297d2540c25f8af5ea3c62e4a80ca7359292d783c04"}, + {file = "blosc2-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:546fa39f397dd54b13d7c42a4f890afaf16c70fe478712070942d464c440ce03"}, + {file = "blosc2-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5455af77e7e94159bb4966cae554f232ca2d52bb80cd3f878ecef39cf569da2a"}, + {file = "blosc2-2.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4dc4f595bf95c350c50bb77a8749cdd08a5dc2bdf3bdb18983d49a52d60b595"}, + {file = "blosc2-2.5.1-cp310-cp310-win32.whl", hash = "sha256:873483bd5c6afb8d139039180ee57b74373232e87b032cb80389fd8bb883ea8e"}, + {file = "blosc2-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d5a7ef00b82fbca069e949335f9c92ce7cbe2039a9fa2e2bd4f5f418043d6262"}, + {file = "blosc2-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da826d42d616f8a939f27e1501b40e764fded66bc80177eeaefcebdbf3b3afb8"}, + {file = "blosc2-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae2e0c5dc8561a6b17842ee4320b49621434c20e622c9e9f5c67c9c6eb3b06a3"}, + {file = "blosc2-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3cab9c12a4364c643266ee7d9583b526c0f484a291d72ec6efb09ea7ffbbf9"}, + {file = "blosc2-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f03a723130cf07e4309fe34b1360c868f4376e862f8ff664eb40d019fdd3f6"}, + {file = "blosc2-2.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0fd109eef815ea1e50fde4f676388aa2f3bb5543502d125fb63f16ec7a014464"}, + {file = "blosc2-2.5.1-cp311-cp311-win32.whl", hash = "sha256:1a3edc3256bad04d3db30c9de7eac3a820f96e741fc754cdabb6a9991e5c37e8"}, + {file = "blosc2-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:e7499e277c13334d54f84e74f429f32341f99f7b978deaf9a7c2e963904cb48c"}, + {file = "blosc2-2.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ab849d3adaeb035f2f16cf495cff1792b28d58dfb3de21b9459ee355c6bb8df3"}, + {file = "blosc2-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd66e60dafcc93d4c1f815d726d76f9fb067ecc9106a6c661010e709135c79ce"}, + {file = "blosc2-2.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb5fcd1775b3884d9825aa51fb45253f45cfa21c77f4135fad5dc5db710c2a34"}, + {file = "blosc2-2.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f79071a336fcf1eda01cd0171291a4ab82b16cf9a15d2b4d26c010146f13b5"}, + {file = "blosc2-2.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:956a63231f1b448803e9b4bc3e704ea424c89fc14418d99093472c74f19c19e1"}, + {file = "blosc2-2.5.1-cp312-cp312-win32.whl", hash = "sha256:5856e57e0e81f9018f1a12e803b9f768fa5533175092d72d165ac60069c7d2ab"}, + {file = "blosc2-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:585d780c5e85f251dec72b75a47666e4a261dbfe1d228769bca545e9fe07f480"}, + {file = "blosc2-2.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cb9a6ac1abc466c12bdc90052f17545512de8f854e672a1ea4d2b40292323f5"}, + {file = "blosc2-2.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3def4650faa1db43143d821228ef58797108cc95d6698c4b1581909cc2b149ca"}, + {file = "blosc2-2.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6efecc1a22da26c73ff5c60d0dc086db1e7edcceb6b360dd193cda893bef28"}, + {file = "blosc2-2.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b473472b977b770aab3bf20d0feeee84ecd5bb8b15a675287e090ce818c1cd40"}, + {file = "blosc2-2.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7afe59d35d93bf8da7db8de43f4d8aef277514de43953c1e5e416ca839b9023a"}, + {file = "blosc2-2.5.1-cp39-cp39-win32.whl", hash = "sha256:4315ae8d467fe91efa0dbe22004e967008f5fe021ebb3945518f5213d7c4511f"}, + {file = "blosc2-2.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:73eb5e569a91fbe67f7dd78efe6a1ca9a54afff2c847db5dfa675bfd6a424f60"}, + {file = "blosc2-2.5.1.tar.gz", hash = "sha256:47d5df50e7286edf81e629ece35f87f13f55c13c5e8545832188c420c75d1659"}, ] [package.dependencies] msgpack = "*" +ndindex = ">=1.4" +numpy = ">=1.20.3" +py-cpuinfo = "*" [[package]] name = "bokeh" -version = "3.1.1" +version = "3.4.0" description = "Interactive plots and applications in the browser from Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "bokeh-3.1.1-py3-none-any.whl", hash = "sha256:a542a076ce326f81bf6d226355458572d39fe8fc9b547eab9728a2f1d71e4bdb"}, - {file = "bokeh-3.1.1.tar.gz", hash = "sha256:ba0fc6bae4352d307541293256dee930a42d0acf92e760c72dc0e7397c3a28e9"}, + {file = "bokeh-3.4.0-py3-none-any.whl", hash = "sha256:d8d9ba026b734317740f90a8a58502d63c76b96c58752fc421ad4aa04df1fbcd"}, + {file = "bokeh-3.4.0.tar.gz", hash = "sha256:9ea6bc407b5e7d04ba7a2f07d8f00e8b6ffe02c2368e707f41bb362a9928569a"}, ] [package.dependencies] -contourpy = ">=1" +contourpy = ">=1.2" Jinja2 = ">=2.9" numpy = ">=1.16" packaging = ">=16.8" pandas = ">=1.2" pillow = ">=7.1.0" PyYAML = ">=3.10" -tornado = ">=5.1" +tornado = ">=6.2" xyzservices = ">=2021.09.1" [[package]] @@ -678,74 +661,66 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "contourpy" -version = "1.1.1" +version = "1.2.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, - {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, - {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, - {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, - {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, - {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, - {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, - {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, - {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, - {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, - {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, - {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, - {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, - {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, - {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, - {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, - {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, - {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, -] - -[package.dependencies] -numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""} +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" @@ -941,89 +916,22 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] -[[package]] -name = "cython" -version = "3.0.10" -description = "The Cython compiler for writing C extensions in the Python language." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "Cython-3.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e876272548d73583e90babda94c1299537006cad7a34e515a06c51b41f8657aa"}, - {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc377aa33c3309191e617bf675fdbb51ca727acb9dc1aa23fc698d8121f7e23"}, - {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:401aba1869a57aba2922ccb656a6320447e55ace42709b504c2f8e8b166f46e1"}, - {file = "Cython-3.0.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:541fbe725d6534a90b93f8c577eb70924d664b227a4631b90a6e0506d1469591"}, - {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:86998b01f6a6d48398df8467292c7637e57f7e3a2ca68655367f13f66fed7734"}, - {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d092c0ddba7e9e530a5c5be4ac06db8360258acc27675d1fc86294a5dc8994c5"}, - {file = "Cython-3.0.10-cp310-cp310-win32.whl", hash = "sha256:3cffb666e649dba23810732497442fb339ee67ba4e0be1f0579991e83fcc2436"}, - {file = "Cython-3.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:9ea31184c7b3a728ef1f81fccb161d8948c05aa86c79f63b74fb6f3ddec860ec"}, - {file = "Cython-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:051069638abfb076900b0c2bcb6facf545655b3f429e80dd14365192074af5a4"}, - {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712760879600907189c7d0d346851525545484e13cd8b787e94bfd293da8ccf0"}, - {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38d40fa1324ac47c04483d151f5e092406a147eac88a18aec789cf01c089c3f2"}, - {file = "Cython-3.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bd49a3a9fdff65446a3e1c2bfc0ec85c6ce4c3cad27cd4ad7ba150a62b7fb59"}, - {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8df79b596633b8295eaa48b1157d796775c2bb078f32267d32f3001b687f2fd"}, - {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bcc9795990e525c192bc5c0775e441d7d56d7a7d02210451e9e13c0448dba51b"}, - {file = "Cython-3.0.10-cp311-cp311-win32.whl", hash = "sha256:09f2000041db482cad3bfce94e1fa3a4c82b0e57390a164c02566cbbda8c4f12"}, - {file = "Cython-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:3919a55ec9b6c7db6f68a004c21c05ed540c40dbe459ced5d801d5a1f326a053"}, - {file = "Cython-3.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f2864ab5fcd27a346f0b50f901ebeb8f60b25a60a575ccfd982e7f3e9674914"}, - {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:407840c56385b9c085826fe300213e0e76ba15d1d47daf4b58569078ecb94446"}, - {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a036d00caa73550a3a976432ef21c1e3fa12637e1616aab32caded35331ae96"}, - {file = "Cython-3.0.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc6a0e7e23a96dec3f3c9d39690d4281beabd5297855140d0d30855f950275e"}, - {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5e14a8c6a8157d2b0cdc2e8e3444905d20a0e78e19d2a097e89fb8b04b51f6b"}, - {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f8a2b8fa0fd8358bccb5f3304be563c4750aae175100463d212d5ea0ec74cbe0"}, - {file = "Cython-3.0.10-cp312-cp312-win32.whl", hash = "sha256:2d29e617fd23cf4b83afe8f93f2966566c9f565918ad1e86a4502fe825cc0a79"}, - {file = "Cython-3.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:6c5af936940a38c300977b81598d9c0901158f220a58c177820e17e1774f1cf1"}, - {file = "Cython-3.0.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f465443917d5c0f69825fca3b52b64c74ac3de0143b1fff6db8ba5b48c9fb4a"}, - {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fadb84193c25641973666e583df8df4e27c52cdc05ddce7c6f6510d690ba34a"}, - {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fa9e7786083b6aa61594c16979d621b62e61fcd9c2edd4761641b95c7fb34b2"}, - {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4780d0f98ce28191c4d841c4358b5d5e79d96520650910cd59904123821c52d"}, - {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:32fbad02d1189be75eb96456d9c73f5548078e5338d8fa153ecb0115b6ee279f"}, - {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:90e2f514fc753b55245351305a399463103ec18666150bb1c36779b9862388e9"}, - {file = "Cython-3.0.10-cp36-cp36m-win32.whl", hash = "sha256:a9c976e9ec429539a4367cb4b24d15a1e46b925976f4341143f49f5f161171f5"}, - {file = "Cython-3.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:a9bb402674788a7f4061aeef8057632ec440123e74ed0fb425308a59afdfa10e"}, - {file = "Cython-3.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:206e803598010ecc3813db8748ed685f7beeca6c413f982df9f8a505fce56563"}, - {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15b6d397f4ee5ad54e373589522af37935a32863f1b23fa8c6922adf833e28e2"}, - {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a181144c2f893ed8e6a994d43d0b96300bc99873f21e3b7334ca26c61c37b680"}, - {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74b700d6a793113d03fb54b63bdbadba6365379424bac7c0470605672769260"}, - {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:076e9fd4e0ca33c5fa00a7479180dbfb62f17fe928e2909f82da814536e96d2b"}, - {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:269f06e6961e8591d56e30b46e1a51b6ccb42cab04c29fa3b30d3e8723485fb4"}, - {file = "Cython-3.0.10-cp37-cp37m-win32.whl", hash = "sha256:d4e83a8ceff7af60064da4ccfce0ac82372544dd5392f1b350c34f1b04d0fae6"}, - {file = "Cython-3.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:40fac59c3a7fbcd9c25aea64c342c890a5e2270ce64a1525e840807800167799"}, - {file = "Cython-3.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f43a58bf2434870d2fc42ac2e9ff8138c9e00c6251468de279d93fa279e9ba3b"}, - {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e9a885ec63d3955a08cefc4eec39fefa9fe14989c6e5e2382bd4aeb6bdb9bc3"}, - {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acfbe0fff364d54906058fc61f2393f38cd7fa07d344d80923937b87e339adcf"}, - {file = "Cython-3.0.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8adcde00a8a88fab27509b558cd8c2959ab0c70c65d3814cfea8c68b83fa6dcd"}, - {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c9c1e3e78909488f3b16fabae02308423fa6369ed96ab1e250807d344cfffd7"}, - {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc6e0faf5b57523b073f0cdefadcaef3a51235d519a0594865925cadb3aeadf0"}, - {file = "Cython-3.0.10-cp38-cp38-win32.whl", hash = "sha256:35f6ede7c74024ed1982832ae61c9fad7cf60cc3f5b8c6a63bb34e38bc291936"}, - {file = "Cython-3.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:950c0c7b770d2a7cec74fb6f5ccc321d0b51d151f48c075c0d0db635a60ba1b5"}, - {file = "Cython-3.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:077b61ee789e48700e25d4a16daa4258b8e65167136e457174df400cf9b4feab"}, - {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f1f8bba9d8f37c0cffc934792b4ac7c42d0891077127c11deebe9fa0a0f7e4"}, - {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd"}, - {file = "Cython-3.0.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d10fc9aa82e5e53a0b7fd118f9771199cddac8feb4a6d8350b7d4109085aa775"}, - {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f610964ab252a83e573a427e28b103e2f1dd3c23bee54f32319f9e73c3c5499"}, - {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c9c4c4f3ab8f8c02817b0e16e8fa7b8cc880f76e9b63fe9c010e60c1a6c2b13"}, - {file = "Cython-3.0.10-cp39-cp39-win32.whl", hash = "sha256:0bac3ccdd4e03924028220c62ae3529e17efa8ca7e9df9330de95de02f582b26"}, - {file = "Cython-3.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:81f356c1c8c0885b8435bfc468025f545c5d764aa9c75ab662616dd1193c331e"}, - {file = "Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2"}, - {file = "Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99"}, -] - [[package]] name = "dask" -version = "2023.5.0" +version = "2024.4.1" description = "Parallel PyData with Task Scheduling" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "dask-2023.5.0-py3-none-any.whl", hash = "sha256:32b34986519b7ddc0947c8ca63c2fc81b964e4c208dfb5cbf9f4f8aec92d152b"}, - {file = "dask-2023.5.0.tar.gz", hash = "sha256:4f4c28ac406e81b8f21b5be4b31b21308808f3e0e7c7e2f4a914f16476d9941b"}, + {file = "dask-2024.4.1-py3-none-any.whl", hash = "sha256:cac5d28b9de7a7cfde46d6fbd8fa81f5654980d010b44d1dbe04dd13b5b63126"}, + {file = "dask-2024.4.1.tar.gz", hash = "sha256:6cd8eb03ddc8dc08d6ca5b167b8de559872bc51cc2b6587d0e9dc754ab19cdf0"}, ] [package.dependencies] -click = ">=8.0" +click = ">=8.1" cloudpickle = ">=1.5.0" fsspec = ">=2021.09.0" -importlib-metadata = ">=4.13.0" +importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} numpy = {version = ">=1.21", optional = true, markers = "extra == \"array\""} packaging = ">=20.0" partd = ">=1.2.0" @@ -1032,11 +940,11 @@ toolz = ">=0.10.0" [package.extras] array = ["numpy (>=1.21)"] -complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)"] -dataframe = ["numpy (>=1.21)", "pandas (>=1.3)"] +complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] +dataframe = ["dask-expr (>=1.0,<1.1)", "dask[array]", "pandas (>=1.3)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2023.5.0)"] -test = ["pandas[test]", "pre-commit", "pytest", "pytest-rerunfailures", "pytest-xdist"] +distributed = ["distributed (==2024.4.1)"] +test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] name = "debugpy" @@ -1185,13 +1093,13 @@ test = ["pytest (>=6)"] [[package]] name = "execnet" -version = "2.1.0" +version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" files = [ - {file = "execnet-2.1.0-py3-none-any.whl", hash = "sha256:ad174d7705410adc9359ba4822bad211d71cdbd59ff70304e1aa41d196b4b4d3"}, - {file = "execnet-2.1.0.tar.gz", hash = "sha256:dc4a63f86afb40f8429f59f938d6cb97846f9e7cf7dd9eb4b8c26016965b7ac0"}, + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, ] [package.extras] @@ -1219,17 +1127,14 @@ optional = false python-versions = ">=3.7" files = [ {file = "fabio-2023.10.0-cp310-cp310-macosx_10_9_arm64.whl", hash = "sha256:d459ad935a7ecb52182958fcac82357c3a6a40aad99c1b18bbd2c3e32bc0e11c"}, - {file = "fabio-2023.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3db70d9675c595db3f80aa207e324987beb8b751ba0decbb9096fde7164714ca"}, {file = "fabio-2023.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e9516ba9653cb9fbeee4566101c3e5319fe37a5fcc4f336ced65a49cf2031e03"}, {file = "fabio-2023.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2882c969e3776ed39749a8f287f57750604df0f496f6b40cb6c23bab237171"}, {file = "fabio-2023.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:4c441fdd0cc55a6dbb3e858f99ed7083a34c4e6b7ffab2cd4c4988affa07a51f"}, {file = "fabio-2023.10.0-cp311-cp311-macosx_10_9_arm64.whl", hash = "sha256:aca8edecdde4d8ef94491aa4e54c2dcd3a56915d6828f2de42e16469347e6cdb"}, - {file = "fabio-2023.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e86de26183d5be77e91ec53d8fddfb31896efa6bdfcfe2078737a731f3f7277"}, {file = "fabio-2023.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea70ec9c2a8e969e867d24cd694c98d89195e3fef895c841b933635e15cc32a"}, {file = "fabio-2023.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1cbe926dd5c9d8cad321876066cc5aa53a92221e2a5556e477e5243193eb39"}, {file = "fabio-2023.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a9975f12d2b117910da8f8c1e42e4527f530b5a0045f35813e8cb2a1ebb4586e"}, {file = "fabio-2023.10.0-cp312-cp312-macosx_10_9_arm64.whl", hash = "sha256:13e26f9daddb542316690370a5eba486267a4efb3b09c1c0cbaec650a39409e3"}, - {file = "fabio-2023.10.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ae09f12aafd9f6187f9b001b96ecbb44b0b041e5f6c42037a8f65b882e9d2a49"}, {file = "fabio-2023.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7d58f315a957132c5477b313b48a4e0a0f7c89139259f38bb7c990486ba4676"}, {file = "fabio-2023.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d6c8784fa3677a0634306fb400edef01f6f15222b6eb843c2b054838bd268839"}, {file = "fabio-2023.10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f6ca9c22c27bc0940b825c34d4e73f067c914b4257f19cb6f0ba7a9599625e9e"}, @@ -1542,36 +1447,32 @@ tornado = ["tornado"] [[package]] name = "h5py" -version = "3.10.0" +version = "3.11.0" description = "Read and write HDF5 files from Python" optional = false python-versions = ">=3.8" files = [ - {file = "h5py-3.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b963fb772964fc1d1563c57e4e2e874022ce11f75ddc6df1a626f42bd49ab99f"}, - {file = "h5py-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:012ab448590e3c4f5a8dd0f3533255bc57f80629bf7c5054cf4c87b30085063c"}, - {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781a24263c1270a62cd67be59f293e62b76acfcc207afa6384961762bb88ea03"}, - {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f42e6c30698b520f0295d70157c4e202a9e402406f50dc08f5a7bc416b24e52d"}, - {file = "h5py-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:93dd840bd675787fc0b016f7a05fc6efe37312a08849d9dd4053fd0377b1357f"}, - {file = "h5py-3.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2381e98af081b6df7f6db300cd88f88e740649d77736e4b53db522d8874bf2dc"}, - {file = "h5py-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:667fe23ab33d5a8a6b77970b229e14ae3bb84e4ea3382cc08567a02e1499eedd"}, - {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90286b79abd085e4e65e07c1bd7ee65a0f15818ea107f44b175d2dfe1a4674b7"}, - {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c013d2e79c00f28ffd0cc24e68665ea03ae9069e167087b2adb5727d2736a52"}, - {file = "h5py-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:92273ce69ae4983dadb898fd4d3bea5eb90820df953b401282ee69ad648df684"}, - {file = "h5py-3.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c97d03f87f215e7759a354460fb4b0d0f27001450b18b23e556e7856a0b21c3"}, - {file = "h5py-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86df4c2de68257b8539a18646ceccdcf2c1ce6b1768ada16c8dcfb489eafae20"}, - {file = "h5py-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9ab36be991119a3ff32d0c7cbe5faf9b8d2375b5278b2aea64effbeba66039"}, - {file = "h5py-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c8e4fda19eb769e9a678592e67eaec3a2f069f7570c82d2da909c077aa94339"}, - {file = "h5py-3.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:492305a074327e8d2513011fa9fffeb54ecb28a04ca4c4227d7e1e9616d35641"}, - {file = "h5py-3.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9450464b458cca2c86252b624279115dcaa7260a40d3cb1594bf2b410a2bd1a3"}, - {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6f6d1384a9f491732cee233b99cd4bfd6e838a8815cc86722f9d2ee64032af"}, - {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3074ec45d3dc6e178c6f96834cf8108bf4a60ccb5ab044e16909580352010a97"}, - {file = "h5py-3.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:212bb997a91e6a895ce5e2f365ba764debeaef5d2dca5c6fb7098d66607adf99"}, - {file = "h5py-3.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dfc65ac21fa2f630323c92453cadbe8d4f504726ec42f6a56cf80c2f90d6c52"}, - {file = "h5py-3.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4682b94fd36ab217352be438abd44c8f357c5449b8995e63886b431d260f3d3"}, - {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aece0e2e1ed2aab076c41802e50a0c3e5ef8816d60ece39107d68717d4559824"}, - {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43a61b2c2ad65b1fabc28802d133eed34debcc2c8b420cb213d3d4ef4d3e2229"}, - {file = "h5py-3.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae2f0201c950059676455daf92700eeb57dcf5caaf71b9e1328e6e6593601770"}, - {file = "h5py-3.10.0.tar.gz", hash = "sha256:d93adc48ceeb33347eb24a634fb787efc7ae4644e6ea4ba733d099605045c049"}, + {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"}, + {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"}, + {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"}, + {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"}, + {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"}, + {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"}, + {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"}, + {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"}, + {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"}, + {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"}, + {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"}, + {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"}, + {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"}, + {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"}, + {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"}, + {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"}, + {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"}, + {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"}, + {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"}, + {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"}, + {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"}, ] [package.dependencies] @@ -1668,13 +1569,13 @@ usid = ["pyUSID (>=0.0.7)", "sidpy"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1880,42 +1781,40 @@ test = ["ipython[test]", "pytest", "pytest-asyncio", "pytest-cov", "testpath"] [[package]] name = "ipython" -version = "8.12.3" +version = "8.18.1" description = "IPython: Productive Interactive Computing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, - {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] [package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] -all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] [[package]] name = "ipython-genutils" @@ -2002,13 +1901,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.3.2" +version = "1.4.0" description = "Lightweight pipelining with Python functions" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, - {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, + {file = "joblib-1.4.0-py3-none-any.whl", hash = "sha256:42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7"}, + {file = "joblib-1.4.0.tar.gz", hash = "sha256:1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c"}, ] [[package]] @@ -2048,11 +1947,9 @@ files = [ attrs = ">=22.2.0" fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} @@ -2076,7 +1973,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] @@ -2343,13 +2239,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.25.4" +version = "2.26.0" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = true python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.25.4-py3-none-any.whl", hash = "sha256:eb645ecc8f9b24bac5decc7803b6d5363250e16ec5af814e516bc2c54dd88081"}, - {file = "jupyterlab_server-2.25.4.tar.gz", hash = "sha256:2098198e1e82e0db982440f9b5136175d73bea2cd42a6480aa6fd502cb23c4f9"}, + {file = "jupyterlab_server-2.26.0-py3-none-any.whl", hash = "sha256:54622cbd330526a385ee0c1fdccdff3a1e7219bf3e864a335284a1270a1973df"}, + {file = "jupyterlab_server-2.26.0.tar.gz", hash = "sha256:9b3ba91cf2837f7f124fca36d63f3ca80ace2bed4898a63dd47e6598c1ab006f"}, ] [package.dependencies] @@ -2547,35 +2443,32 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "llvmlite" -version = "0.41.1" +version = "0.42.0" description = "lightweight wrapper around basic LLVM functionality" optional = false -python-versions = ">=3.8" -files = [ - {file = "llvmlite-0.41.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1e1029d47ee66d3a0c4d6088641882f75b93db82bd0e6178f7bd744ebce42b9"}, - {file = "llvmlite-0.41.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:150d0bc275a8ac664a705135e639178883293cf08c1a38de3bbaa2f693a0a867"}, - {file = "llvmlite-0.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eee5cf17ec2b4198b509272cf300ee6577229d237c98cc6e63861b08463ddc6"}, - {file = "llvmlite-0.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd0338da625346538f1173a17cabf21d1e315cf387ca21b294ff209d176e244"}, - {file = "llvmlite-0.41.1-cp310-cp310-win32.whl", hash = "sha256:fa1469901a2e100c17eb8fe2678e34bd4255a3576d1a543421356e9c14d6e2ae"}, - {file = "llvmlite-0.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:2b76acee82ea0e9304be6be9d4b3840208d050ea0dcad75b1635fa06e949a0ae"}, - {file = "llvmlite-0.41.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:210e458723436b2469d61b54b453474e09e12a94453c97ea3fbb0742ba5a83d8"}, - {file = "llvmlite-0.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:855f280e781d49e0640aef4c4af586831ade8f1a6c4df483fb901cbe1a48d127"}, - {file = "llvmlite-0.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b67340c62c93a11fae482910dc29163a50dff3dfa88bc874872d28ee604a83be"}, - {file = "llvmlite-0.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2181bb63ef3c607e6403813421b46982c3ac6bfc1f11fa16a13eaafb46f578e6"}, - {file = "llvmlite-0.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:9564c19b31a0434f01d2025b06b44c7ed422f51e719ab5d24ff03b7560066c9a"}, - {file = "llvmlite-0.41.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5940bc901fb0325970415dbede82c0b7f3e35c2d5fd1d5e0047134c2c46b3281"}, - {file = "llvmlite-0.41.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8b0a9a47c28f67a269bb62f6256e63cef28d3c5f13cbae4fab587c3ad506778b"}, - {file = "llvmlite-0.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8afdfa6da33f0b4226af8e64cfc2b28986e005528fbf944d0a24a72acfc9432"}, - {file = "llvmlite-0.41.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8454c1133ef701e8c050a59edd85d238ee18bb9a0eb95faf2fca8b909ee3c89a"}, - {file = "llvmlite-0.41.1-cp38-cp38-win32.whl", hash = "sha256:2d92c51e6e9394d503033ffe3292f5bef1566ab73029ec853861f60ad5c925d0"}, - {file = "llvmlite-0.41.1-cp38-cp38-win_amd64.whl", hash = "sha256:df75594e5a4702b032684d5481db3af990b69c249ccb1d32687b8501f0689432"}, - {file = "llvmlite-0.41.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04725975e5b2af416d685ea0769f4ecc33f97be541e301054c9f741003085802"}, - {file = "llvmlite-0.41.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf14aa0eb22b58c231243dccf7e7f42f7beec48970f2549b3a6acc737d1a4ba4"}, - {file = "llvmlite-0.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92c32356f669e036eb01016e883b22add883c60739bc1ebee3a1cc0249a50828"}, - {file = "llvmlite-0.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24091a6b31242bcdd56ae2dbea40007f462260bc9bdf947953acc39dffd54f8f"}, - {file = "llvmlite-0.41.1-cp39-cp39-win32.whl", hash = "sha256:880cb57ca49e862e1cd077104375b9d1dfdc0622596dfa22105f470d7bacb309"}, - {file = "llvmlite-0.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:92f093986ab92e71c9ffe334c002f96defc7986efda18397d0f08534f3ebdc4d"}, - {file = "llvmlite-0.41.1.tar.gz", hash = "sha256:f19f767a018e6ec89608e1f6b13348fa2fcde657151137cb64e56d48598a92db"}, +python-versions = ">=3.9" +files = [ + {file = "llvmlite-0.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3366938e1bf63d26c34fbfb4c8e8d2ded57d11e0567d5bb243d89aab1eb56098"}, + {file = "llvmlite-0.42.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c35da49666a21185d21b551fc3caf46a935d54d66969d32d72af109b5e7d2b6f"}, + {file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f44ccc3c6220bd23e0ba698a63ec2a7d3205da0d848804807f37fc243e3f77"}, + {file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f8d8717a9073b9e0246998de89929071d15b47f254c10eef2310b9aac033d"}, + {file = "llvmlite-0.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d90edf400b4ceb3a0e776b6c6e4656d05c7187c439587e06f86afceb66d2be5"}, + {file = "llvmlite-0.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae511caed28beaf1252dbaf5f40e663f533b79ceb408c874c01754cafabb9cbf"}, + {file = "llvmlite-0.42.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81e674c2fe85576e6c4474e8c7e7aba7901ac0196e864fe7985492b737dbab65"}, + {file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3975787f13eb97629052edb5017f6c170eebc1c14a0433e8089e5db43bcce6"}, + {file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5bece0cdf77f22379f19b1959ccd7aee518afa4afbd3656c6365865f84903f9"}, + {file = "llvmlite-0.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e0c4c11c8c2aa9b0701f91b799cb9134a6a6de51444eff5a9087fc7c1384275"}, + {file = "llvmlite-0.42.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:08fa9ab02b0d0179c688a4216b8939138266519aaa0aa94f1195a8542faedb56"}, + {file = "llvmlite-0.42.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2fce7d355068494d1e42202c7aff25d50c462584233013eb4470c33b995e3ee"}, + {file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe66a86dc44634b59a3bc860c7b20d26d9aaffcd30364ebe8ba79161a9121f4"}, + {file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47494552559e00d81bfb836cf1c4d5a5062e54102cc5767d5aa1e77ccd2505c"}, + {file = "llvmlite-0.42.0-cp312-cp312-win_amd64.whl", hash = "sha256:05cb7e9b6ce69165ce4d1b994fbdedca0c62492e537b0cc86141b6e2c78d5888"}, + {file = "llvmlite-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bdd3888544538a94d7ec99e7c62a0cdd8833609c85f0c23fcb6c5c591aec60ad"}, + {file = "llvmlite-0.42.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0936c2067a67fb8816c908d5457d63eba3e2b17e515c5fe00e5ee2bace06040"}, + {file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a78ab89f1924fc11482209f6799a7a3fc74ddc80425a7a3e0e8174af0e9e2301"}, + {file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7599b65c7af7abbc978dbf345712c60fd596aa5670496561cc10e8a71cebfb2"}, + {file = "llvmlite-0.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:43d65cc4e206c2e902c1004dd5418417c4efa6c1d04df05c6c5675a27e8ca90e"}, + {file = "llvmlite-0.42.0.tar.gz", hash = "sha256:f92b09243c0cc3f457da8b983f67bd8e1295d0f5b3746c7a1861d7a99403854a"}, ] [[package]] @@ -2879,58 +2772,39 @@ files = [ [[package]] name = "matplotlib" -version = "3.7.5" +version = "3.8.4" description = "Python plotting package" optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:4a87b69cb1cb20943010f63feb0b2901c17a3b435f75349fd9865713bfa63925"}, - {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d3ce45010fefb028359accebb852ca0c21bd77ec0f281952831d235228f15810"}, - {file = "matplotlib-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbea1e762b28400393d71be1a02144aa16692a3c4c676ba0178ce83fc2928fdd"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec0e1adc0ad70ba8227e957551e25a9d2995e319c29f94a97575bb90fa1d4469"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6738c89a635ced486c8a20e20111d33f6398a9cbebce1ced59c211e12cd61455"}, - {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1210b7919b4ed94b5573870f316bca26de3e3b07ffdb563e79327dc0e6bba515"}, - {file = "matplotlib-3.7.5-cp310-cp310-win32.whl", hash = "sha256:068ebcc59c072781d9dcdb82f0d3f1458271c2de7ca9c78f5bd672141091e9e1"}, - {file = "matplotlib-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:f098ffbaab9df1e3ef04e5a5586a1e6b1791380698e84938d8640961c79b1fc0"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f65342c147572673f02a4abec2d5a23ad9c3898167df9b47c149f32ce61ca078"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ddf7fc0e0dc553891a117aa083039088d8a07686d4c93fb8a810adca68810af"}, - {file = "matplotlib-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ccb830fc29442360d91be48527809f23a5dcaee8da5f4d9b2d5b867c1b087b8"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc6bb28178e844d1f408dd4d6341ee8a2e906fc9e0fa3dae497da4e0cab775d"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b15c4c2d374f249f324f46e883340d494c01768dd5287f8bc00b65b625ab56c"}, - {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d028555421912307845e59e3de328260b26d055c5dac9b182cc9783854e98fb"}, - {file = "matplotlib-3.7.5-cp311-cp311-win32.whl", hash = "sha256:fe184b4625b4052fa88ef350b815559dd90cc6cc8e97b62f966e1ca84074aafa"}, - {file = "matplotlib-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:084f1f0f2f1010868c6f1f50b4e1c6f2fb201c58475494f1e5b66fed66093647"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:34bceb9d8ddb142055ff27cd7135f539f2f01be2ce0bafbace4117abe58f8fe4"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c5a2134162273eb8cdfd320ae907bf84d171de948e62180fa372a3ca7cf0f433"}, - {file = "matplotlib-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:039ad54683a814002ff37bf7981aa1faa40b91f4ff84149beb53d1eb64617980"}, - {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d742ccd1b09e863b4ca58291728db645b51dab343eebb08d5d4b31b308296ce"}, - {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:743b1c488ca6a2bc7f56079d282e44d236bf375968bfd1b7ba701fd4d0fa32d6"}, - {file = "matplotlib-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:fbf730fca3e1f23713bc1fae0a57db386e39dc81ea57dc305c67f628c1d7a342"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cfff9b838531698ee40e40ea1a8a9dc2c01edb400b27d38de6ba44c1f9a8e3d2"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:1dbcca4508bca7847fe2d64a05b237a3dcaec1f959aedb756d5b1c67b770c5ee"}, - {file = "matplotlib-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4cdf4ef46c2a1609a50411b66940b31778db1e4b73d4ecc2eaa40bd588979b13"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:167200ccfefd1674b60e957186dfd9baf58b324562ad1a28e5d0a6b3bea77905"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:53e64522934df6e1818b25fd48cf3b645b11740d78e6ef765fbb5fa5ce080d02"}, - {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e3bc79b2d7d615067bd010caff9243ead1fc95cf735c16e4b2583173f717eb"}, - {file = "matplotlib-3.7.5-cp38-cp38-win32.whl", hash = "sha256:6b641b48c6819726ed47c55835cdd330e53747d4efff574109fd79b2d8a13748"}, - {file = "matplotlib-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:f0b60993ed3488b4532ec6b697059897891927cbfc2b8d458a891b60ec03d9d7"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:090964d0afaff9c90e4d8de7836757e72ecfb252fb02884016d809239f715651"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9fc6fcfbc55cd719bc0bfa60bde248eb68cf43876d4c22864603bdd23962ba25"}, - {file = "matplotlib-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7cc3078b019bb863752b8b60e8b269423000f1603cb2299608231996bd9d54"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4e9a868e8163abaaa8259842d85f949a919e1ead17644fb77a60427c90473c"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa7ebc995a7d747dacf0a717d0eb3aa0f0c6a0e9ea88b0194d3a3cd241a1500f"}, - {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3785bfd83b05fc0e0c2ae4c4a90034fe693ef96c679634756c50fe6efcc09856"}, - {file = "matplotlib-3.7.5-cp39-cp39-win32.whl", hash = "sha256:29b058738c104d0ca8806395f1c9089dfe4d4f0f78ea765c6c704469f3fffc81"}, - {file = "matplotlib-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:fd4028d570fa4b31b7b165d4a685942ae9cdc669f33741e388c01857d9723eab"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2a9a3f4d6a7f88a62a6a18c7e6a84aedcaf4faf0708b4ca46d87b19f1b526f88"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b3fd853d4a7f008a938df909b96db0b454225f935d3917520305b90680579c"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ad550da9f160737d7890217c5eeed4337d07e83ca1b2ca6535078f354e7675"}, - {file = "matplotlib-3.7.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20da7924a08306a861b3f2d1da0d1aa9a6678e480cf8eacffe18b565af2813e7"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b45c9798ea6bb920cb77eb7306409756a7fab9db9b463e462618e0559aecb30e"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a99866267da1e561c7776fe12bf4442174b79aac1a47bd7e627c7e4d077ebd83"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6aa62adb6c268fc87d80f963aca39c64615c31830b02697743c95590ce3fbb"}, - {file = "matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286"}, - {file = "matplotlib-3.7.5.tar.gz", hash = "sha256:1e5c971558ebc811aa07f54c7b7c677d78aa518ef4c390e14673a09e0860184a"}, +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"}, + {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"}, + {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"}, + {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"}, + {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"}, + {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"}, + {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"}, + {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"}, + {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"}, + {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"}, + {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"}, ] [package.dependencies] @@ -2938,10 +2812,10 @@ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20,<2" +kiwisolver = ">=1.3.1" +numpy = ">=1.21" packaging = ">=20.0" -pillow = ">=6.2.0" +pillow = ">=8" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" @@ -3341,6 +3215,20 @@ nbformat = "*" sphinx = ">=1.8" traitlets = ">=5" +[[package]] +name = "ndindex" +version = "1.8" +description = "A Python library for manipulating indices of ndarrays." +optional = false +python-versions = ">=3.8" +files = [ + {file = "ndindex-1.8-py3-none-any.whl", hash = "sha256:b5132cd331f3e4106913ed1a974a3e355967a5991543c2f512b40cb8bb9f50b8"}, + {file = "ndindex-1.8.tar.gz", hash = "sha256:5fc87ebc784605f01dd5367374cb40e8da8f2c30988968990066c5098a7eebe8"}, +] + +[package.extras] +arrays = ["numpy"] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -3354,57 +3242,57 @@ files = [ [[package]] name = "networkx" -version = "3.1" +version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, - {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, ] [package.extras] -default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] -developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] -test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "niondata" -version = "0.15.3" +version = "15.6.2" description = "A data processing library for Nion Swift." optional = false -python-versions = "~=3.8" +python-versions = ">=3.9" files = [ - {file = "niondata-0.15.3-py3-none-any.whl", hash = "sha256:b4eb68fd515544148fb95c54b6deb3f6026c1271f55015db56f90f852e7c614f"}, - {file = "niondata-0.15.3.tar.gz", hash = "sha256:8016e17c7efe6542f1d7f90fac5e1259ccf352e4b9ec9992a33cf9bb31fe513a"}, + {file = "niondata-15.6.2-py3-none-any.whl", hash = "sha256:62cee1895b3993e1e13a017121f2804b362697b6bd831833d6335566a0a89e64"}, + {file = "niondata-15.6.2.tar.gz", hash = "sha256:1298e680ff6dcbecfff61451a8568920690dba9c5ac3966695d9e1442ca4d546"}, ] [package.dependencies] -nionutils = ">=0.4.5,<0.5.0" +nionutils = ">=0.4.5,<5.0.0" numpy = ">=1.21,<2.0" scipy = "*" [[package]] name = "nionswift" -version = "0.16.8" +version = "16.10.0" description = "Nion Swift: Scientific Image Processing." optional = false -python-versions = "<3.12,>=3.8" +python-versions = ">=3.9" files = [ - {file = "nionswift-0.16.8-py3-none-any.whl", hash = "sha256:ac49ac01fb6c77939dba9502cc7a18dd964071983730998598a50e1ff7c80cb7"}, - {file = "nionswift-0.16.8.tar.gz", hash = "sha256:d407476bdf1bf188a0dc4fa7b9981b7cd5ea1cdc21ab5eba9202c565cd5e5fd0"}, + {file = "nionswift-16.10.0-py3-none-any.whl", hash = "sha256:6c40b770bdeffaa24e6d9075c843e813530e566f148210a7f706f06ceab16bde"}, + {file = "nionswift-16.10.0.tar.gz", hash = "sha256:1e3c6d7fbe806b50b5ffd80c8c6352fa9e1c3f6f61d6c426bcda0c8729dc558a"}, ] [package.dependencies] h5py = "*" imageio = ">=2.19.0" -niondata = ">=0.15.3,<0.16.0" -nionswift-io = ">=0.15.0" -nionui = ">=0.6.6,<0.7.0" -nionutils = ">=0.4.5,<0.5.0" -numpy = ">=1.21,<2.0" +niondata = ">=15.6.2,<16.0" +nionswift-io = ">=15.2,<16.0" +nionui = ">=7.0.2,<8.0" +nionutils = ">=0.4.10,<5.0" +numpy = ">=1.26,<2.0" pillow = "*" pytz = "*" scipy = "*" @@ -3412,46 +3300,46 @@ tzlocal = "*" [[package]] name = "nionswift-io" -version = "0.15.1" +version = "15.2.1" description = "NionSwift IO handlers." optional = false -python-versions = "~=3.8" +python-versions = ">=3.9" files = [ - {file = "nionswift-io-0.15.1.tar.gz", hash = "sha256:105519b04e4a454c4bf64245a5b1b419d64fe13805c4dcdeb9bf311d7a5fb7b8"}, - {file = "nionswift_io-0.15.1-py3-none-any.whl", hash = "sha256:e6d4eebb51e11d501fe78313d07747bfe77b677ef00931c8f203c559b178230f"}, + {file = "nionswift-io-15.2.1.tar.gz", hash = "sha256:d40696c8802d67c348eed69bb6882f088b99f9adefc53c1c26701eca725c1d18"}, + {file = "nionswift_io-15.2.1-py3-none-any.whl", hash = "sha256:602fd74ba561929ed4317a32393461e1c8cb9091b94c40703a41092042fe4697"}, ] [package.dependencies] imageio = "*" -niondata = ">=0.14.0,<0.16.0" -nionutils = ">=0.4.0,<0.5.0" -numpy = ">=1.21,<2.0" +niondata = ">=15.6,<16.0" +nionutils = ">=0.4.0,<5.0.0" +numpy = ">=1.25,<2.0" [[package]] name = "nionui" -version = "0.6.10" +version = "7.0.3" description = "Nion UI framework." optional = false -python-versions = "~=3.8" +python-versions = ">=3.9" files = [ - {file = "nionui-0.6.10-py3-none-any.whl", hash = "sha256:497defc0687ffb4eaa4e48ba7fc7606fe6573b94a0b3c4b0ab0d4fcf35ab2aea"}, - {file = "nionui-0.6.10.tar.gz", hash = "sha256:7aba19cbd17a7761194770053a71c45c7701585865336dc7cbc97c146d314461"}, + {file = "nionui-7.0.3-py3-none-any.whl", hash = "sha256:ca3a8e78f527ac641183b2e3936bbba26f15c051e6cc1b7a05242bf1ce5307e9"}, + {file = "nionui-7.0.3.tar.gz", hash = "sha256:1d81ec9ace0d4df1ec1731a32f72aff66860ccbdbb88098f7b964cd18e35a434"}, ] [package.dependencies] imageio = ">=2.19.0" -nionutils = ">=0.4.5,<0.5.0" +nionutils = ">=0.4.7,<5.0.0" numpy = ">=1.21,<2.0" [[package]] name = "nionutils" -version = "0.4.6" +version = "0.4.10" description = "Nion utility classes." optional = false -python-versions = "~=3.8" +python-versions = ">=3.9" files = [ - {file = "nionutils-0.4.6-py3-none-any.whl", hash = "sha256:9a3ac99a45b17cd2f7a39fd68ea3de833133c05791ec7c0558726419bfd94abf"}, - {file = "nionutils-0.4.6.tar.gz", hash = "sha256:f8010d901de664e30fa4dd4ae8039cb3c332b3842df3ea5adfa816e940bed01e"}, + {file = "nionutils-0.4.10-py3-none-any.whl", hash = "sha256:7eea639227a621f727b951e06bfdca00ec50767a3a16dcb1918121dd32387671"}, + {file = "nionutils-0.4.10.tar.gz", hash = "sha256:cd74a23d10a45c3d24ec2bd5b766b28ee35d42ec5ed5aa730442321f13337fed"}, ] [[package]] @@ -3507,37 +3395,36 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numba" -version = "0.58.1" +version = "0.59.1" description = "compiling Python code using LLVM" optional = false -python-versions = ">=3.8" -files = [ - {file = "numba-0.58.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07f2fa7e7144aa6f275f27260e73ce0d808d3c62b30cff8906ad1dec12d87bbe"}, - {file = "numba-0.58.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7bf1ddd4f7b9c2306de0384bf3854cac3edd7b4d8dffae2ec1b925e4c436233f"}, - {file = "numba-0.58.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc2d904d0319d7a5857bd65062340bed627f5bfe9ae4a495aef342f072880d50"}, - {file = "numba-0.58.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e79b6cc0d2bf064a955934a2e02bf676bc7995ab2db929dbbc62e4c16551be6"}, - {file = "numba-0.58.1-cp310-cp310-win_amd64.whl", hash = "sha256:81fe5b51532478149b5081311b0fd4206959174e660c372b94ed5364cfb37c82"}, - {file = "numba-0.58.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bcecd3fb9df36554b342140a4d77d938a549be635d64caf8bd9ef6c47a47f8aa"}, - {file = "numba-0.58.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1eaa744f518bbd60e1f7ccddfb8002b3d06bd865b94a5d7eac25028efe0e0ff"}, - {file = "numba-0.58.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bf68df9c307fb0aa81cacd33faccd6e419496fdc621e83f1efce35cdc5e79cac"}, - {file = "numba-0.58.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55a01e1881120e86d54efdff1be08381886fe9f04fc3006af309c602a72bc44d"}, - {file = "numba-0.58.1-cp311-cp311-win_amd64.whl", hash = "sha256:811305d5dc40ae43c3ace5b192c670c358a89a4d2ae4f86d1665003798ea7a1a"}, - {file = "numba-0.58.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea5bfcf7d641d351c6a80e8e1826eb4a145d619870016eeaf20bbd71ef5caa22"}, - {file = "numba-0.58.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e63d6aacaae1ba4ef3695f1c2122b30fa3d8ba039c8f517784668075856d79e2"}, - {file = "numba-0.58.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fe7a9d8e3bd996fbe5eac0683227ccef26cba98dae6e5cee2c1894d4b9f16c1"}, - {file = "numba-0.58.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:898af055b03f09d33a587e9425500e5be84fc90cd2f80b3fb71c6a4a17a7e354"}, - {file = "numba-0.58.1-cp38-cp38-win_amd64.whl", hash = "sha256:d3e2fe81fe9a59fcd99cc572002101119059d64d31eb6324995ee8b0f144a306"}, - {file = "numba-0.58.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c765aef472a9406a97ea9782116335ad4f9ef5c9f93fc05fd44aab0db486954"}, - {file = "numba-0.58.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e9356e943617f5e35a74bf56ff6e7cc83e6b1865d5e13cee535d79bf2cae954"}, - {file = "numba-0.58.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:240e7a1ae80eb6b14061dc91263b99dc8d6af9ea45d310751b780888097c1aaa"}, - {file = "numba-0.58.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45698b995914003f890ad839cfc909eeb9c74921849c712a05405d1a79c50f68"}, - {file = "numba-0.58.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd3dda77955be03ff366eebbfdb39919ce7c2620d86c906203bed92124989032"}, - {file = "numba-0.58.1.tar.gz", hash = "sha256:487ded0633efccd9ca3a46364b40006dbdaca0f95e99b8b83e778d1195ebcbaa"}, -] - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} -llvmlite = "==0.41.*" +python-versions = ">=3.9" +files = [ + {file = "numba-0.59.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97385a7f12212c4f4bc28f648720a92514bee79d7063e40ef66c2d30600fd18e"}, + {file = "numba-0.59.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b77aecf52040de2a1eb1d7e314497b9e56fba17466c80b457b971a25bb1576d"}, + {file = "numba-0.59.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3476a4f641bfd58f35ead42f4dcaf5f132569c4647c6f1360ccf18ee4cda3990"}, + {file = "numba-0.59.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:525ef3f820931bdae95ee5379c670d5c97289c6520726bc6937a4a7d4230ba24"}, + {file = "numba-0.59.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e395e44d192a12105eca3083b61307db7da10e093972ca285c85bef0963d6"}, + {file = "numba-0.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43727e7ad20b3ec23ee4fc642f5b61845c71f75dd2825b3c234390c6d8d64051"}, + {file = "numba-0.59.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:411df625372c77959570050e861981e9d196cc1da9aa62c3d6a836b5cc338966"}, + {file = "numba-0.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2801003caa263d1e8497fb84829a7ecfb61738a95f62bc05693fcf1733e978e4"}, + {file = "numba-0.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd2842fac03be4e5324ebbbd4d2d0c8c0fc6e0df75c09477dd45b288a0777389"}, + {file = "numba-0.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:0594b3dfb369fada1f8bb2e3045cd6c61a564c62e50cf1f86b4666bc721b3450"}, + {file = "numba-0.59.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1cce206a3b92836cdf26ef39d3a3242fec25e07f020cc4feec4c4a865e340569"}, + {file = "numba-0.59.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c8b4477763cb1fbd86a3be7050500229417bf60867c93e131fd2626edb02238"}, + {file = "numba-0.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d80bce4ef7e65bf895c29e3889ca75a29ee01da80266a01d34815918e365835"}, + {file = "numba-0.59.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7ad1d217773e89a9845886401eaaab0a156a90aa2f179fdc125261fd1105096"}, + {file = "numba-0.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bf68f4d69dd3a9f26a9b23548fa23e3bcb9042e2935257b471d2a8d3c424b7f"}, + {file = "numba-0.59.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e0318ae729de6e5dbe64c75ead1a95eb01fabfe0e2ebed81ebf0344d32db0ae"}, + {file = "numba-0.59.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f68589740a8c38bb7dc1b938b55d1145244c8353078eea23895d4f82c8b9ec1"}, + {file = "numba-0.59.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:649913a3758891c77c32e2d2a3bcbedf4a69f5fea276d11f9119677c45a422e8"}, + {file = "numba-0.59.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9712808e4545270291d76b9a264839ac878c5eb7d8b6e02c970dc0ac29bc8187"}, + {file = "numba-0.59.1-cp39-cp39-win_amd64.whl", hash = "sha256:8d51ccd7008a83105ad6a0082b6a2b70f1142dc7cfd76deb8c5a862367eb8c86"}, + {file = "numba-0.59.1.tar.gz", hash = "sha256:76f69132b96028d2774ed20415e8c528a34e3299a40581bae178f0994a2f370b"}, +] + +[package.dependencies] +llvmlite = "==0.42.*" numpy = ">=1.22,<1.27" [[package]] @@ -3582,81 +3469,88 @@ zfpy = ["zfpy (>=1.0.0)"] [[package]] name = "numexpr" -version = "2.8.5" +version = "2.10.0" description = "Fast numerical expression evaluator for NumPy" optional = false -python-versions = ">=3.7" -files = [ - {file = "numexpr-2.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51f3ab160c3847ebcca93cd88f935a7802b54a01ab63fe93152994a64d7a6cf2"}, - {file = "numexpr-2.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:de29c77f674e4eb8f0846525a475cab64008c227c8bc4ba5153ab3f72441cc63"}, - {file = "numexpr-2.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf85ba1327eb87ec82ae7936f13c8850fb969a0ca34f3ba9fa3897c09d5c80d7"}, - {file = "numexpr-2.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c00be69f747f44a631830215cab482f0f77f75af2925695adff57c1cc0f9a68"}, - {file = "numexpr-2.8.5-cp310-cp310-win32.whl", hash = "sha256:c46350dcdb93e32f033eea5a21269514ffcaf501d9abd6036992d37e48a308b0"}, - {file = "numexpr-2.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:894b027438b8ec88dea32a19193716c79f4ff8ddb92302dcc9731b51ba3565a8"}, - {file = "numexpr-2.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6df184d40d4cf9f21c71f429962f39332f7398147762588c9f3a5c77065d0c06"}, - {file = "numexpr-2.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:178b85ad373c6903e55d75787d61b92380439b70d94b001cb055a501b0821335"}, - {file = "numexpr-2.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:578fe4008e4d5d6ff01bbeb2d7b7ba1ec658a5cda9c720cd26a9a8325f8ef438"}, - {file = "numexpr-2.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef621b4ee366a5c6a484f6678c9259f5b826569f8bfa0b89ba2306d5055468bb"}, - {file = "numexpr-2.8.5-cp311-cp311-win32.whl", hash = "sha256:dd57ab1a3d3aaa9274aff1cefbf93b8ddacc7973afef5b125905f6bf18fabab0"}, - {file = "numexpr-2.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:783324ba40eb804ecfc9ebae86120a1e339ab112d0ab8a1f0d48a26354d5bf9b"}, - {file = "numexpr-2.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:183d5430db76826e54465c69db93a3c6ecbf03cda5aa1bb96eaad0147e9b68dc"}, - {file = "numexpr-2.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39ce106f92ccea5b07b1d6f2f3c4370f05edf27691dc720a63903484a2137e48"}, - {file = "numexpr-2.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b594dc9e2d6291a0bc5c065e6d9caf3eee743b5663897832e9b17753c002947a"}, - {file = "numexpr-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:62b4faf8e0627673b0210a837792bddd23050ecebc98069ab23eb0633ff1ef5f"}, - {file = "numexpr-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:db5c65417d69414f1ab31302ea01d3548303ef31209c38b4849d145be4e1d1ba"}, - {file = "numexpr-2.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb36ffcfa1606e41aa08d559b4277bcad0e16b83941d1a4fee8d2bd5a34f8e0e"}, - {file = "numexpr-2.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34af2a0e857d02a4bc5758bc037a777d50dacb13bcd57c7905268a3e44994ed6"}, - {file = "numexpr-2.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a8dad2bfaad5a5c34a2e8bbf62b9df1dfab266d345fda1feb20ff4e264b347a"}, - {file = "numexpr-2.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93f5a866cd13a808bc3d3a9c487d94cd02eec408b275ff0aa150f2e8e5191f8"}, - {file = "numexpr-2.8.5-cp38-cp38-win32.whl", hash = "sha256:558390fea6370003ac749ed9d0f38d708aa096f5dcb707ddb6e0ca5a0dd37da1"}, - {file = "numexpr-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:55983806815035eb63c5039520688c49536bb7f3cc3fc1d7d64c6a00cf3f353e"}, - {file = "numexpr-2.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1510da20e6f5f45333610b1ded44c566e2690c6c437c84f2a212ca09627c7e01"}, - {file = "numexpr-2.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e8b5bf7bcb4e8dcd66522d8fc96e1db7278f901cb4fd2e155efbe62a41dde08"}, - {file = "numexpr-2.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ed0e1c1ef5f34381448539f1fe9015906d21c9cfa2797c06194d4207dadb465"}, - {file = "numexpr-2.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aea6ab45c87c0a7041183c08a798f0ad4d7c5eccbce20cfe79ce6f1a45ef3702"}, - {file = "numexpr-2.8.5-cp39-cp39-win32.whl", hash = "sha256:cbfd833ee5fdb0efb862e152aee7e6ccea9c596d5c11d22604c2e6307bff7cad"}, - {file = "numexpr-2.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:283ce8609a7ccbadf91a68f3484558b3e36d27c93c98a41ec205efb0ab43c872"}, - {file = "numexpr-2.8.5.tar.gz", hash = "sha256:45ed41e55a0abcecf3d711481e12a5fb7a904fe99d42bc282a17cc5f8ea510be"}, -] - -[package.dependencies] -numpy = ">=1.13.3" +python-versions = ">=3.9" +files = [ + {file = "numexpr-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1af6dc6b3bd2e11a802337b352bf58f30df0b70be16c4f863b70a3af3a8ef95e"}, + {file = "numexpr-2.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c66dc0188358cdcc9465b6ee54fd5eef2e83ac64b1d4ba9117c41df59bf6fca"}, + {file = "numexpr-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83f1e7a7f7ee741b8dcd20c56c3f862a3a3ec26fa8b9fcadb7dcd819876d2f35"}, + {file = "numexpr-2.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f0b045e1831953a47cc9fabae76a6794c69cbb60921751a5cf2d555034c55bf"}, + {file = "numexpr-2.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d8eb88b0ae3d3c609d732a17e71096779b2bf47b3a084320ffa93d9f9132786"}, + {file = "numexpr-2.10.0-cp310-cp310-win32.whl", hash = "sha256:629b66cc1b750671e7fb396506b3f9410612e5bd8bc1dd55b5a0a0041d839f95"}, + {file = "numexpr-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:78e0a8bc4417c3dedcbae3c473505b69080535246edc977c7dccf3ec8454a685"}, + {file = "numexpr-2.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a602692cd52ce923ce8a0a90fb1d6cf186ebe8706eed83eee0de685e634b9aa9"}, + {file = "numexpr-2.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:745b46a1fb76920a3eebfaf26e50bc94a9c13b5aee34b256ab4b2d792dbaa9ca"}, + {file = "numexpr-2.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10789450032357afaeda4ac4d06da9542d1535c13151e8d32b49ae1a488d1358"}, + {file = "numexpr-2.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4feafc65ea3044b8bf8f305b757a928e59167a310630c22b97a57dff07a56490"}, + {file = "numexpr-2.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:937d36c6d3cf15601f26f84f0f706649f976491e9e0892d16cd7c876d77fa7dc"}, + {file = "numexpr-2.10.0-cp311-cp311-win32.whl", hash = "sha256:03d0ba492e484a5a1aeb24b300c4213ed168f2c246177be5733abb4e18cbb043"}, + {file = "numexpr-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:6b5f8242c075477156d26b3a6b8e0cd0a06d4c8eb68d907bde56dd3c9c683e92"}, + {file = "numexpr-2.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b276e2ba3e87ace9a30fd49078ad5dcdc6a1674d030b1ec132599c55465c0346"}, + {file = "numexpr-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb5e12787101f1216f2cdabedc3417748f2e1f472442e16bbfabf0bab2336300"}, + {file = "numexpr-2.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05278bad96b5846d712eba58b44e5cec743bdb3e19ca624916c921d049fdbcf6"}, + {file = "numexpr-2.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cdf9e64c5b3dbb61729edb505ea75ee212fa02b85c5b1d851331381ae3b0e1"}, + {file = "numexpr-2.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e3a973265591b0a875fd1151c4549e468959c7192821aac0bb86937694a08efa"}, + {file = "numexpr-2.10.0-cp312-cp312-win32.whl", hash = "sha256:416e0e9f0fc4cced67767585e44cb6b301728bdb9edbb7c534a853222ec62cac"}, + {file = "numexpr-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:748e8d4cde22d9a5603165293fb293a4de1a4623513299416c64fdab557118c2"}, + {file = "numexpr-2.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc3506c30c03b082da2cadef43747d474e5170c1f58a6dcdf882b3dc88b1e849"}, + {file = "numexpr-2.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:efa63ecdc9fcaf582045639ddcf56e9bdc1f4d9a01729be528f62df4db86c9d6"}, + {file = "numexpr-2.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a64d0dd8f8e694da3f8582d73d7da8446ff375f6dd239b546010efea371ac3"}, + {file = "numexpr-2.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47bb567e330ebe86781864219a36cbccb3a47aec893bd509f0139c6b23e8104"}, + {file = "numexpr-2.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c7517b774d309b1f0896c89bdd1ddd33c4418a92ecfbe5e1df3ac698698f6fcf"}, + {file = "numexpr-2.10.0-cp39-cp39-win32.whl", hash = "sha256:04e8620e7e676504201d4082e7b3ee2d9b561d1cb9470b47a6104e10c1e2870e"}, + {file = "numexpr-2.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:56d0d96b130f7cd4d78d0017030d6a0e9d9fc2a717ac51d4cf4860b39637e86a"}, + {file = "numexpr-2.10.0.tar.gz", hash = "sha256:c89e930752639df040539160326d8f99a84159bbea41943ab8e960591edaaef0"}, +] + +[package.dependencies] +numpy = ">=1.19.3" [[package]] name = "numpy" -version = "1.24.4" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -3744,12 +3638,12 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\" and python_version >= \"3.8\""}, - {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.17.3", markers = "(platform_system != \"Darwin\" and platform_system != \"Linux\") and python_version >= \"3.8\" and python_version < \"3.9\" or platform_system != \"Darwin\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_machine != \"aarch64\" or platform_machine != \"arm64\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_system != \"Linux\" or (platform_machine != \"arm64\" and platform_machine != \"aarch64\") and python_version >= \"3.8\" and python_version < \"3.9\""}, + {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, + {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] @@ -3854,70 +3748,73 @@ files = [ [[package]] name = "pandas" -version = "2.0.3" +version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"}, - {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"}, - {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"}, - {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"}, - {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"}, - {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"}, - {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"}, - {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"}, - {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"}, - {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"}, - {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"}, - {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"}, - {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"}, - {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"}, - {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"}, - {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"}, - {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"}, - {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, ] [package.dependencies] numpy = [ - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"] -aws = ["s3fs (>=2021.08.0)"] -clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"] -compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"] -computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2021.07.0)"] -gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"] -hdf5 = ["tables (>=3.6.1)"] -html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"] -mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"] -spss = ["pyreadstat (>=1.1.2)"] -sql-other = ["SQLAlchemy (>=1.4.16)"] -test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.6.3)"] +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "pandocfilters" @@ -3979,54 +3876,39 @@ ptyprocess = ">=0.5" [[package]] name = "photutils" -version = "1.8.0" +version = "1.11.0" description = "An Astropy package for source detection and photometry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "photutils-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71af12251a5362f847f56be5b0698ffad2691c62723470ee7b08aa9c68c7ca27"}, - {file = "photutils-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb3fcd3c0a2dd763b8aed51a0ae606f50f0ec5d793298d942e1056ed0ab5b6e7"}, - {file = "photutils-1.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf0095a1ed4d11a85e99e90141166e7df5f7512435ddbf52bbe781aedee0f13"}, - {file = "photutils-1.8.0-cp310-cp310-win32.whl", hash = "sha256:1e4c5b0e26ccd07611c0c342907214d7343950e2233dd4cdd29552c18edecd24"}, - {file = "photutils-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:622a791ae5cec6b68a04f3d255688df32e6b401e18eb3d4a70e7d5150561d652"}, - {file = "photutils-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0da859cdd4cc25d6fb9ef5086f15b088c1673c348e1d786559e6f7365a964c6"}, - {file = "photutils-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f224c27e620a404e4a58a9406ef83fdf0af50f01939f1ce52617c8ae6d0bb7d"}, - {file = "photutils-1.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ce5ff0c03c4c426671da4a6a738279682f1dc2a352c77006561debc403955b7"}, - {file = "photutils-1.8.0-cp311-cp311-win32.whl", hash = "sha256:3748821c96c5f0345dbce96989b4ed57144f7885866bc1d3bd142a0db709663e"}, - {file = "photutils-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e1119ae00cbf7a461f92182abc46ef4e78e689f0e8e7f6183e76ab88a8028fc"}, - {file = "photutils-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b46cf6c29533d7dd19902cabca79447699714fb393722234bcd3f8fe58c442d"}, - {file = "photutils-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd77a07b18d826cb8434ed1d2545213a11901618dd1a6a814269876d62b415b7"}, - {file = "photutils-1.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c2c6d01056f3bbce470d38475f9a30ee625bb212522e62fa093a5ed0917ca52"}, - {file = "photutils-1.8.0-cp38-cp38-win32.whl", hash = "sha256:9c8102e7e8f7dab3312d9c41d54f485c9ec8bc6f00c84d6ccf6d2526b54c0adc"}, - {file = "photutils-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:948249587bc61efbfb6c8bc0ed16dd7e60fa06b64bf904ba75d397556d2f333f"}, - {file = "photutils-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33369290b4aa658fce73a7700430d22530c0fc3776d28609ae4e8afa287d12c7"}, - {file = "photutils-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b0f948223d20499caad9d01c91e95facce6028e70e76d6683a85f3d72f3f2eff"}, - {file = "photutils-1.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1736e3d108a55eaf769e4c8bbf988f923bf5ddcac41e5c0f7664de48868010c"}, - {file = "photutils-1.8.0-cp39-cp39-win32.whl", hash = "sha256:f05a52daa4041476aece812589e9b4bbaef582a159945ef83b9463f600de3082"}, - {file = "photutils-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:d539269c4e4e350ffa1202d24935f48e0bad48e0d4200c6319392f36bcba4e2e"}, - {file = "photutils-1.8.0.tar.gz", hash = "sha256:2ccf397659f48109e760924c5a4cd324caf9e8cb86aa6946ebfc074547a3701e"}, -] - -[package.dependencies] -astropy = ">=5.0" -numpy = ">=1.21" + {file = "photutils-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d4f089f65fe9aeb3095ccf40e4784ea47a1a26e48888f1e11b1b87191b13d90"}, + {file = "photutils-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5d907ecc5511aa56108c2c8deece625628effcb8224676f4874b03d5e673bbee"}, + {file = "photutils-1.11.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fa60fc13976e8d4983ee8a8f071ec6703ef492a2761bff2b37e5e0549c5881a"}, + {file = "photutils-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c1fe7906f3bfe6d870efdf659b4f7e8c34aca959122805f0102a735df948002"}, + {file = "photutils-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a59bc830fbe083651b2a0709a6b2069bb8ef70a9e6025c9cf64cfe47b2abfa11"}, + {file = "photutils-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6419525f3c2a9dbe7c71a6e1e2e3dfec17ccb2a8cb3ed6f93b52dec25a6d5fe8"}, + {file = "photutils-1.11.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4f93bfdb12b32cad475f2bee4cb3e2187ac13f83ce0b06f12f17569f4da2b9"}, + {file = "photutils-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:3cd5bb00eab472193d467f6d0385842117948e7f842d9e254563ff06935ccdad"}, + {file = "photutils-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3699d65f1e9697da84a413a4cfbd00a91730b44ef79664ae0423016fdf38ece0"}, + {file = "photutils-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43fa6b850a42700621f1083b639cb7c1e55abccf16db76274dc33ba1a877c64d"}, + {file = "photutils-1.11.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a130a515794156897e74859e255dd3f880c12233495a1cd34295295da2c34e35"}, + {file = "photutils-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:2514f2a6a1da90f30e0a3a2cd538bfcc281dbddb60e6ec5a3b665e761d8835d9"}, + {file = "photutils-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53b251ebd5ffab83035d04f79bc65e18c64479b36d1fe0ef8817a4606b3c595b"}, + {file = "photutils-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e18b24319bd16f096b1dfd821b3e5e672bce7dbbf18bc32c2e51c53f7b43660b"}, + {file = "photutils-1.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8530421f849a0aa158aed02aa3cd7cd8c18843402eefd38ebd34565f7b985e18"}, + {file = "photutils-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:f2680ea011d5955384c9b1b5e2730018bd45ebaafc462ed2893666ba6313f886"}, + {file = "photutils-1.11.0.tar.gz", hash = "sha256:ee709d090bac2bc8b8b317078c87f9c4b6855f6f94c2f7f2a93ab5b8f8375597"}, +] + +[package.dependencies] +astropy = ">=5.1" +numpy = ">=1.22" [package.extras] -all = ["bottleneck", "gwcs (>=0.18)", "matplotlib (>=3.5.0)", "rasterio", "scikit-image (>=0.19.0)", "scikit-learn (>=1.0)", "scipy (>=1.7.0)", "shapely", "tqdm"] -docs = ["gwcs (>=0.18)", "matplotlib (>=3.5.0)", "rasterio", "scikit-image (>=0.19.0)", "scikit-learn (>=1.0)", "scipy (>=1.7.0)", "shapely", "sphinx", "sphinx-astropy (>=1.6)"] +all = ["bottleneck", "gwcs (>=0.18)", "matplotlib (>=3.5)", "rasterio", "scikit-image (>=0.19)", "scikit-learn (>=1.0)", "scipy (>=1.7.2)", "shapely", "tqdm"] +docs = ["photutils[all]", "sphinx", "sphinx-astropy (>=1.6)", "tomli"] test = ["pytest-astropy (>=0.10)"] -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -optional = false -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - [[package]] name = "pillow" version = "10.3.0" @@ -4115,36 +3997,30 @@ xmp = ["defusedxml"] [[package]] name = "pint" -version = "0.21.1" +version = "0.23" description = "Physical quantities module" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "Pint-0.21.1-py3-none-any.whl", hash = "sha256:230ebccc312693117ee925c6492b3631c772ae9f7851a4e86080a15e7be692d8"}, - {file = "Pint-0.21.1.tar.gz", hash = "sha256:5d5b6b518d0c5a7ab03a776175db500f1ed1523ee75fb7fafe38af8149431c8d"}, + {file = "Pint-0.23-py3-none-any.whl", hash = "sha256:df79b6b5f1beb7ed0cd55d91a0766fc55f972f757a9364e844958c05e8eb66f9"}, + {file = "Pint-0.23.tar.gz", hash = "sha256:e1509b91606dbc52527c600a4ef74ffac12fff70688aff20e9072409346ec9b4"}, ] +[package.dependencies] +typing-extensions = "*" + [package.extras] babel = ["babel (<=2.8)"] +bench = ["pytest", "pytest-codspeed"] dask = ["dask"] mip = ["mip (>=1.13)"] numpy = ["numpy (>=1.19.5)"] pandas = ["pint-pandas (>=0.3)"] -test = ["pytest", "pytest-cov", "pytest-mpl", "pytest-subtests"] +test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mpl", "pytest-subtests"] +testbase = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-subtests"] uncertainties = ["uncertainties (>=3.1.6)"] xarray = ["xarray"] -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "platformdirs" version = "4.2.0" @@ -4414,55 +4290,26 @@ files = [ [[package]] name = "pyerfa" -version = "2.0.0.3" +version = "2.0.1.3" description = "Python bindings for ERFA" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pyerfa-2.0.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:676515861ca3f0cb9d7e693389233e7126413a5ba93a0cc4d36b8ca933951e8d"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a438865894d226247dcfcb60d683ae075a52716504537052371b2b73458fe4fc"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73bf7d23f069d47632a2feeb1e73454b10392c4f3c16116017a6983f1f0e9b2b"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:780b0f90adf500b8ba24e9d509a690576a7e8287e354cfb90227c5963690d3fc"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5447bb45ddedde3052693c86b941a4908f5dbeb4a697bda45b5b89de92cfb74a"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c24e7960c6cdd3fa3f4dba5f3444a106ad48c94ff0b19eebaee06a142c18c52"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-win32.whl", hash = "sha256:170a83bd0243da518119b846f296cf33fa03f1f884a88578c1a38560182cf64e"}, - {file = "pyerfa-2.0.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:51aa6e0faa4aa9ad8f0eef1c47fec76c5bebc0da7023a436089bdd6e5cfd625f"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fa9fceeb78057bfff7ae3aa6cdad3f1b193722de22bdbb75319256f4a9e2f76"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a8a2029fc62ff2369d01219f66a5ce6aed35ef33eddb06118b6c27e8573a9ed8"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da888da2c8db5a78273fbf0af4e74f04e2d312d371c3c021cf6c3b14fa60fe3b"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354753addba5261ec1cbf1ba45784ed3a5c42da565ecc6e0aa36b7a17fa4689"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b55f7278c1dd362648d7956e1a5365ade5fed2fe5541b721b3ceb5271128892"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:23e5efcf96ed7161d74f79ca261d255e1f36988843d22cd97d8f60fe9c868d44"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-win32.whl", hash = "sha256:f0e9d0b122c454bcad5dbd0c3283b200783031d3f99ca9c550f49a7a7d4c41ea"}, - {file = "pyerfa-2.0.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:09af83540e23a7d61a8368b0514b3daa4ed967e1e52d0add4f501f58c500dd7f"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a07444fd53a5dd18d7955f86f8d9b1be9a68ceb143e1145c0019a310c913c04"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf7364e475cff1f973e2fcf6962de9df9642c8802b010e29b2c592ae337e3c5"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8458421166f6ffe2e259aaf4aaa6e802d6539649a40e3194a81d30dccdc167a"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96ea688341176ae6220cc4743cda655549d71e3e3b60c5a99d02d5912d0ddf55"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d56f6b5a0a3ed7b80d630041829463a872946df277259b5453298842d42a54a4"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-win32.whl", hash = "sha256:3ecb598924ddb4ea2b06efc6f1e55ca70897ed178a690e2eaa1e290448466c7c"}, - {file = "pyerfa-2.0.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1033fdb890ec70d3a511e20a464afc8abbea2180108f27b14d8f1d1addc38cbe"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d8c0dbb17119e52def33f9d6dbf2deaf2113ed3e657b6ff692df9b6a3598397"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a1edd2cbe4ead3bf9a51e578d5d83bdd7ab3b3ccb69e09b89a4c42aa5b35ffb"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04c3b715c924b6f972dd440a94a701a16a07700bc8ba9e88b1df765bdc36ad0"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d01c341c45b860ee5c7585ef003118c8015e9d65c30668d2f5bf657e1dcdd68"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24d89ead30edc6038408336ad9b696683e74c4eef550708fca6afef3ecd5b010"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b8c5e74d48a505a014e855cd4c7be11604901d94fd6f34b685f6720b7b20ed8"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-win32.whl", hash = "sha256:2ccba04de166d81bdd3adcf10428d908ce2f3a56ed1c2767d740fec12680edbd"}, - {file = "pyerfa-2.0.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3df87743e27588c5bd5e1f3a886629b3277fdd418059ca048420d33169376775"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88aa1acedf298d255cc4b0740ee11a3b303b71763dba2f039d48abf0a95cf9df"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06d4f08e96867b1fc3ae9a9e4b38693ed0806463288efc41473ad16e14774504"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1819e0d95ff8dead80614f8063919d82b2dbb55437b6c0109d3393c1ab55954"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61f1097ac2ee8c15a2a636cdfb99340d708574d66f4610456bd457d1e6b852f4"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f42ee01a62c6cbba58103e6f8e600b21ad3a71262dccf03d476efb4a20ea71"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ecd6167b48bb8f1922fae7b49554616f2e7382748a4320ad46ebd7e2cc62f3d"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-win32.whl", hash = "sha256:7f9eabfefa5317ce58fe22480102902f10f270fc64a5636c010f7c0b7e0fb032"}, - {file = "pyerfa-2.0.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:4ea7ca03ecc440224c2bed8fb136fadf6cf8aea8ba67d717f635116f30c8cc8c"}, - {file = "pyerfa-2.0.0.3.tar.gz", hash = "sha256:d77fbbfa58350c194ccb99e5d93aa05d3c2b14d5aad8b662d93c6ad9fff41f39"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc554151de564b567e391b7c9c3b545efac63674ab1954382d38f886254c01fb"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:359327c88f1e5dea3974b284dabef141824ac54753c5cab6b3f23acd9d52071b"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c3a971a9fba8663b49dcc54c3419e837837140d81cc6be9f1c21fc56322f7b"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4472d2a2622e47d220a9436c953a487d8c051157f7b44b1f71964de17ee443b"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b0f621f26b5f31b3fb6bb113fb48a428e56eb00c7d729a242672dc4f886c8d18"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-win32.whl", hash = "sha256:b7a85ac9d807ea71550e831e873916ed3a44300fe6e20e0b3ca0f2784c0b2757"}, + {file = "pyerfa-2.0.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:60c0a73db5a42927fbafd12c623699c2c1b1233b6e1be1963970a5ad47e463c4"}, + {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:779caac3737da68f4db43b0dec026ac479719e02d25b8c4e7b0756abadbcd416"}, + {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:053ed25fdb7deb9d3d7cebecbb3d3dfbeea37c8c0011cc0616293e03d2c308eb"}, + {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ef6c5d2206f134bd95329a0c17d46c449c9b68e9828e97e9bc43b29cd8789f5d"}, + {file = "pyerfa-2.0.1.3.tar.gz", hash = "sha256:ba5eb932341beaf222726de8dce2b1645c97b48c321efb2af8a535a7eb90ebfa"}, ] [package.dependencies] -numpy = ">=1.17" +numpy = ">=1.19" [package.extras] docs = ["sphinx-astropy (>=1.3)"] @@ -4751,46 +4598,8 @@ files = [ ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version >= \"3.6\" and python_version < \"3.9\""} tzdata = {version = "*", markers = "python_version >= \"3.6\""} -[[package]] -name = "pywavelets" -version = "1.4.1" -description = "PyWavelets, wavelet transform module" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"}, - {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - [[package]] name = "pywin32" version = "306" @@ -4831,37 +4640,41 @@ files = [ [[package]] name = "pyxem" -version = "0.15.1" +version = "0.16.0" description = "multi-dimensional diffraction microscopy" optional = false python-versions = ">=3.7" files = [ - {file = "pyxem-0.15.1-py3-none-any.whl", hash = "sha256:ab2ff237d0125e8f182bd62d78cc4e6a2493d802af1064b64e05019670a92e94"}, - {file = "pyxem-0.15.1.tar.gz", hash = "sha256:0d657ee57600c2e111191463720127bce98aa845ae361dafa2ddeb664c019d73"}, + {file = "pyxem-0.16.0-py3-none-any.whl", hash = "sha256:4e20cbbe5116eec69f344e549e041d401a6a0b5a69e54984875e0d3ab2466707"}, + {file = "pyxem-0.16.0.tar.gz", hash = "sha256:6be051f28530af117de27ff0f62c6843a9eaf96430bff18ba23ab16744b93684"}, ] [package.dependencies] dask = "*" diffsims = ">=0.5" -hyperspy = ">=1.7.0" -ipywidgets = "*" +h5py = "*" +hyperspy = ">=1.7.0,<2.0rc0" lmfit = ">=0.9.12" matplotlib = ">=3.3" numba = "*" +numexpr = "!=2.8.6" numpy = "*" orix = ">=0.9" psutil = "*" pyfai = "*" -scikit-image = ">=0.19.0" +scikit-image = ">=0.19.0,<0.21.0 || >0.21.0" scikit-learn = ">=1.0" scipy = "*" +tqdm = "*" +traits = "*" transforms3d = "*" [package.extras] +dask = ["dask-image", "distributed"] dev = ["black", "pre-commit (>=1.16)"] -doc = ["furo", "nbsphinx (>=0.7)", "sphinx (>=3.0.2)", "sphinx-autodoc-typehints (>=1.10.3)", "sphinx-copybutton (>=0.2.5)", "sphinx-gallery (>=0.6)", "sphinxcontrib-bibtex (>=1.0)"] +doc = ["dask-image", "hyperspy-gui-ipywidgets", "nbsphinx (>=0.7)", "pydata-sphinx-theme", "sphinx (>=3.0.2)", "sphinx-autodoc-typehints (>=1.10.3)", "sphinx-codeautolink", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery (>=0.6)", "sphinxcontrib-bibtex (>=1.0)"] gpu = ["cupy (>=9.0.0)"] -tests = ["coverage (>=5.0)", "coveralls (>=1.10)", "pytest (>=5.0)", "pytest-cov (>=2.8.1)", "pytest-xdist"] +tests = ["coverage (>=5.0)", "coveralls (>=1.10)", "pytest (>=5.0)", "pytest-cov (>=2.8.1)", "pytest-rerunfailures", "pytest-xdist"] [[package]] name = "pyyaml" @@ -4888,7 +4701,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -5064,7 +4876,6 @@ files = [ ] [package.dependencies] -importlib-resources = {version = ">=1.4", markers = "python_version < \"3.9\""} matplotlib = "*" networkx = "*" numpy = "*" @@ -5180,7 +4991,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -5321,138 +5131,135 @@ files = [ [[package]] name = "scikit-image" -version = "0.21.0" +version = "0.22.0" description = "Image processing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit_image-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:978ac3302252155a8556cdfe067bad2d18d5ccef4e91c2f727bc564ed75566bc"}, - {file = "scikit_image-0.21.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:82c22e008527e5ee26ab08e3ce919998ef164d538ff30b9e5764b223cfda06b1"}, - {file = "scikit_image-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd29d2631d3e975c377066acfc1f4cb2cc95e2257cf70e7fedfcb96441096e88"}, - {file = "scikit_image-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6c12925ceb9f3aede555921e26642d601b2d37d1617002a2636f2cb5178ae2f"}, - {file = "scikit_image-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f538d4de77e4f3225d068d9ea2965bed3f7dda7f457a8f89634fa22ffb9ad8c"}, - {file = "scikit_image-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec9bab6920ac43037d7434058b67b5778d42c60f67b8679239f48a471e7ed6f8"}, - {file = "scikit_image-0.21.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:a54720430dba833ffbb6dedd93d9f0938c5dd47d20ab9ba3e4e61c19d95f6f19"}, - {file = "scikit_image-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e40dd102da14cdadc09210f930b4556c90ff8f99cd9d8bcccf9f73a86c44245"}, - {file = "scikit_image-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff5719c7eb99596a39c3e1d9b564025bae78ecf1da3ee6842d34f6965b5f1474"}, - {file = "scikit_image-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:146c3824253eee9ff346c4ad10cb58376f91aefaf4a4bb2fe11aa21691f7de76"}, - {file = "scikit_image-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4e1b09f81a99c9c390215929194847b3cd358550b4b65bb6e42c5393d69cb74a"}, - {file = "scikit_image-0.21.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9f7b5fb4a22f0d5ae0fa13beeb887c925280590145cd6d8b2630794d120ff7c7"}, - {file = "scikit_image-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4814033717f0b6491fee252facb9df92058d6a72ab78dd6408a50f3915a88b8"}, - {file = "scikit_image-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0d6ed6502cca0c9719c444caafa0b8cda0f9e29e01ca42f621a240073284be"}, - {file = "scikit_image-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:9194cb7bc21215fde6c1b1e9685d312d2aa8f65ea9736bc6311126a91c860032"}, - {file = "scikit_image-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54df1ddc854f37a912d42bc724e456e86858107e94048a81a27720bc588f9937"}, - {file = "scikit_image-0.21.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:c01e3ab0a1fabfd8ce30686d4401b7ed36e6126c9d4d05cb94abf6bdc46f7ac9"}, - {file = "scikit_image-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ef5d8d1099317b7b315b530348cbfa68ab8ce32459de3c074d204166951025c"}, - {file = "scikit_image-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b1e96c59cab640ca5c5b22c501524cfaf34cbe0cb51ba73bd9a9ede3fb6e1d"}, - {file = "scikit_image-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:9cffcddd2a5594c0a06de2ae3e1e25d662745a26f94fda31520593669677c010"}, - {file = "scikit_image-0.21.0.tar.gz", hash = "sha256:b33e823c54e6f11873ea390ee49ef832b82b9f70752c8759efd09d5a4e3d87f0"}, +python-versions = ">=3.9" +files = [ + {file = "scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d"}, + {file = "scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff"}, + {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6"}, + {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938"}, + {file = "scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc"}, + {file = "scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2"}, + {file = "scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2"}, + {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd"}, + {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e"}, + {file = "scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b"}, + {file = "scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff"}, + {file = "scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9"}, + {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452"}, + {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a"}, + {file = "scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2"}, + {file = "scikit_image-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22318b35044cfeeb63ee60c56fc62450e5fe516228138f1d06c7a26378248a86"}, + {file = "scikit_image-0.22.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9e801c44a814afdadeabf4dffdffc23733e393767958b82319706f5fa3e1eaa9"}, + {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c472a1fb3665ec5c00423684590631d95f9afcbc97f01407d348b821880b2cb3"}, + {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a"}, + {file = "scikit_image-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:5071b8f6341bfb0737ab05c8ab4ac0261f9e25dbcc7b5d31e5ed230fd24a7929"}, + {file = "scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236"}, ] [package.dependencies] imageio = ">=2.27" -lazy_loader = ">=0.2" +lazy_loader = ">=0.3" networkx = ">=2.8" -numpy = ">=1.21.1" +numpy = ">=1.22" packaging = ">=21" pillow = ">=9.0.1" -PyWavelets = ">=1.1.1" scipy = ">=1.8" tifffile = ">=2022.8.12" [package.extras] -build = ["Cython (>=0.29.32)", "build", "meson-python (>=0.13)", "ninja", "numpy (>=1.21.1)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.3)", "wheel"] +build = ["Cython (>=0.29.32)", "build", "meson-python (>=0.14)", "ninja", "numpy (>=1.22)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.6)", "wheel"] data = ["pooch (>=1.6.0)"] -default = ["PyWavelets (>=1.1.1)", "imageio (>=2.27)", "lazy_loader (>=0.2)", "networkx (>=2.8)", "numpy (>=1.21.1)", "packaging (>=21)", "pillow (>=9.0.1)", "scipy (>=1.8)", "tifffile (>=2022.8.12)"] -developer = ["pre-commit", "rtoml"] -docs = ["dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.5)", "myst-parser", "numpydoc (>=1.5)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.13)", "pytest-runner", "scikit-learn (>=0.24.0)", "seaborn (>=0.11)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-gallery (>=0.11)", "sphinx_design (>=0.3)", "tifffile (>=2022.8.12)"] -optional = ["SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=0.24.0)"] -test = ["asv", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-faulthandler", "pytest-localserver"] +developer = ["pre-commit", "tomli"] +docs = ["PyWavelets (>=1.1.1)", "dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.5)", "myst-parser", "numpydoc (>=1.6)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.14.1)", "pytest-runner", "scikit-learn (>=1.1)", "seaborn (>=0.11)", "sphinx (>=7.2)", "sphinx-copybutton", "sphinx-gallery (>=0.14)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] +optional = ["PyWavelets (>=1.1.1)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=1.1)"] +test = ["asv", "matplotlib (>=3.5)", "numpydoc (>=1.5)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-faulthandler", "pytest-localserver"] [[package]] name = "scikit-learn" -version = "1.3.2" +version = "1.4.2" description = "A set of python modules for machine learning and data mining" optional = false -python-versions = ">=3.8" -files = [ - {file = "scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05"}, - {file = "scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1"}, - {file = "scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a"}, - {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c"}, - {file = "scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161"}, - {file = "scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c"}, - {file = "scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66"}, - {file = "scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157"}, - {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb"}, - {file = "scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433"}, - {file = "scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b"}, - {file = "scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028"}, - {file = "scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5"}, - {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525"}, - {file = "scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c"}, - {file = "scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107"}, - {file = "scikit_learn-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93"}, - {file = "scikit_learn-1.3.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073"}, - {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d"}, - {file = "scikit_learn-1.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf"}, - {file = "scikit_learn-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0"}, - {file = "scikit_learn-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03"}, - {file = "scikit_learn-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e"}, - {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a"}, - {file = "scikit_learn-1.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9"}, - {file = "scikit_learn-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0"}, -] - -[package.dependencies] -joblib = ">=1.1.1" -numpy = ">=1.17.3,<2.0" -scipy = ">=1.5.0" +python-versions = ">=3.9" +files = [ + {file = "scikit-learn-1.4.2.tar.gz", hash = "sha256:daa1c471d95bad080c6e44b4946c9390a4842adc3082572c20e4f8884e39e959"}, + {file = "scikit_learn-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8539a41b3d6d1af82eb629f9c57f37428ff1481c1e34dddb3b9d7af8ede67ac5"}, + {file = "scikit_learn-1.4.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:68b8404841f944a4a1459b07198fa2edd41a82f189b44f3e1d55c104dbc2e40c"}, + {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81bf5d8bbe87643103334032dd82f7419bc8c8d02a763643a6b9a5c7288c5054"}, + {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f0ea5d0f693cb247a073d21a4123bdf4172e470e6d163c12b74cbb1536cf38"}, + {file = "scikit_learn-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:87440e2e188c87db80ea4023440923dccbd56fbc2d557b18ced00fef79da0727"}, + {file = "scikit_learn-1.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45dee87ac5309bb82e3ea633955030df9bbcb8d2cdb30383c6cd483691c546cc"}, + {file = "scikit_learn-1.4.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1d0b25d9c651fd050555aadd57431b53d4cf664e749069da77f3d52c5ad14b3b"}, + {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0203c368058ab92efc6168a1507d388d41469c873e96ec220ca8e74079bf62e"}, + {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c62f2b124848a28fd695db5bc4da019287abf390bfce602ddc8aa1ec186aae"}, + {file = "scikit_learn-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:5cd7b524115499b18b63f0c96f4224eb885564937a0b3477531b2b63ce331904"}, + {file = "scikit_learn-1.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90378e1747949f90c8f385898fff35d73193dfcaec3dd75d6b542f90c4e89755"}, + {file = "scikit_learn-1.4.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ff4effe5a1d4e8fed260a83a163f7dbf4f6087b54528d8880bab1d1377bd78be"}, + {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:671e2f0c3f2c15409dae4f282a3a619601fa824d2c820e5b608d9d775f91780c"}, + {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36d0bc983336bbc1be22f9b686b50c964f593c8a9a913a792442af9bf4f5e68"}, + {file = "scikit_learn-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:d762070980c17ba3e9a4a1e043ba0518ce4c55152032f1af0ca6f39b376b5928"}, + {file = "scikit_learn-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9993d5e78a8148b1d0fdf5b15ed92452af5581734129998c26f481c46586d68"}, + {file = "scikit_learn-1.4.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:426d258fddac674fdf33f3cb2d54d26f49406e2599dbf9a32b4d1696091d4256"}, + {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5460a1a5b043ae5ae4596b3126a4ec33ccba1b51e7ca2c5d36dac2169f62ab1d"}, + {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d64ef6cb8c093d883e5a36c4766548d974898d378e395ba41a806d0e824db8"}, + {file = "scikit_learn-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:c97a50b05c194be9146d61fe87dbf8eac62b203d9e87a3ccc6ae9aed2dfaf361"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" threadpoolctl = ">=2.0.0" [package.extras] -benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] +benchmark = ["matplotlib (>=3.3.4)", "memory-profiler (>=0.57.0)", "pandas (>=1.1.5)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.15.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +tests = ["black (>=23.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.19.12)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.17.2)"] [[package]] name = "scipy" -version = "1.10.1" +version = "1.13.0" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = "<3.12,>=3.8" +python-versions = ">=3.9" files = [ - {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"}, - {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"}, - {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"}, - {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"}, - {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"}, - {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"}, - {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"}, - {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"}, - {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"}, - {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"}, - {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"}, - {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"}, - {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"}, - {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"}, - {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"}, - {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"}, - {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"}, - {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"}, - {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"}, - {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"}, - {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"}, + {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"}, + {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"}, + {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"}, + {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"}, + {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"}, + {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"}, + {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"}, + {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"}, + {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"}, + {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"}, + {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"}, ] [package.dependencies] -numpy = ">=1.19.5,<1.27.0" +numpy = ">=1.22.4,<2.3" [package.extras] -dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "send2trash" @@ -5607,13 +5414,13 @@ tox = ["sparse[tests]", "tox"] [[package]] name = "sphinx" -version = "7.1.2" +version = "7.2.6" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, - {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, + {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, + {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, ] [package.dependencies] @@ -5625,7 +5432,7 @@ imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.13" +Pygments = ">=2.14" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" @@ -5633,31 +5440,31 @@ sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autodoc-typehints" -version = "2.0.0" +version = "2.0.1" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false python-versions = ">=3.8" files = [ - {file = "sphinx_autodoc_typehints-2.0.0-py3-none-any.whl", hash = "sha256:12c0e161f6fe191c2cdfd8fa3caea271f5387d9fbc67ebcd6f4f1f24ce880993"}, - {file = "sphinx_autodoc_typehints-2.0.0.tar.gz", hash = "sha256:7f2cdac2e70fd9787926b6e9e541cd4ded1e838d2b46fda2a1bb0a75ec5b7f3a"}, + {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, + {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, ] [package.dependencies] sphinx = ">=7.1.2" [package.extras] -docs = ["furo (>=2023.9.10)"] +docs = ["furo (>=2024.1.29)"] numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.8)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" @@ -5680,47 +5487,50 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "1.0.8" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "1.0.6" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.0.5" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -5753,32 +5563,34 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "1.0.7" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "1.1.10" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -5839,40 +5651,39 @@ mpmath = ">=0.19" [[package]] name = "tables" -version = "3.8.0" +version = "3.9.2" description = "Hierarchical datasets for Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "tables-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:01e82e40f9845f71de137b4472210909e35c440bbcd0858bdd2871715daef4c7"}, - {file = "tables-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db185d855afd45a7259ddd0b53e5f2f8993bb134b370002c6c19532f27ce92ac"}, - {file = "tables-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70a3585a268beee6d0e71bfc9abec98da84d168182f350a2ffa1ae5e42798c18"}, - {file = "tables-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:117cf0f73ee2a5cba5c2b04e4aca375779aec66045aa63128e043dc608f2023b"}, - {file = "tables-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2861cd3ef9eb95eead7530e4de49fd130954871e7e6d2e288012797cb9d7c2e8"}, - {file = "tables-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e9bdbfbe025b6c751976382123c5f5cbd8fab6956aed776b0e8c889669e90d3"}, - {file = "tables-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0821007048f2af8c1a21eb3d832072046c5df366e39587a7c7e4afad14e73fc"}, - {file = "tables-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b9370c2a4dc0051aad6b71de4f1f9b0b8b60d30b662df5c742434f2b5c6a005e"}, - {file = "tables-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e19686fad4e8f5a91c3dc1eb4b7ea928838e86fefa474c63c5787a125ea79fc7"}, - {file = "tables-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:239f15fa9881c257b5c0d9fb4cb8832778af1c5c8c1db6f6722466f8f26541e2"}, - {file = "tables-3.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a74cac3c0629a0e83570d465f88843ef3609ef56a8ef9a49ee85ab3b8f02f"}, - {file = "tables-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:a5ccb80651c5fad6ac744e2a756b28cfac78eab3b8503f4a2320ee6653b3bee9"}, - {file = "tables-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3375bfafc6cf305d13617a572ab3fffc51fae2fbe0f6efce9407a41f79970b62"}, - {file = "tables-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:254a4d5c2009c7ebe4293b02b8d91ea60837bff85a3c0a40cd075b8f12b1e6c3"}, - {file = "tables-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3c96456c473fb977cf6dbca9e889710ac020df1fa5b9ebb7f676e83996337d"}, - {file = "tables-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:72da9404094ef8277bf62fce8873e8dc141cee9a8763ec8e7080b2d0de206094"}, - {file = "tables-3.8.0.tar.gz", hash = "sha256:34f3fa2366ce20b18f1df573a77c1d27306ce1f2a41d9f9eff621b5192ea8788"}, -] - -[package.dependencies] -blosc2 = ">=2.0.0,<2.1.0" -cython = ">=0.29.21" +python-versions = ">=3.9" +files = [ + {file = "tables-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a4e71fc9d2a3a0cacce4994afd47cd5f4797093ff9cee2cc7dc87e51f308107"}, + {file = "tables-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbea426ce9bdd60cda435a265823b31d18f2b36e9045fb2d565679825a7aa46"}, + {file = "tables-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e756c272bb111d016fec1d03a60095403a8fb42a5fbaf5f317dcf6e3b9d8e92e"}, + {file = "tables-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:eea41cb32dd22b30d6f3dd4e113f6d693384d301c89f3c4b4712f90c9c955875"}, + {file = "tables-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d71913fb8147dc6132595b94fc82f88f6c2436a3b5c57aadfe26c680f96aa387"}, + {file = "tables-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d6bbc477d038a17c5062ab6ccd94c8b1fa365cf017b9a2ad6c2dff1a07abb2b"}, + {file = "tables-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e67c71070b871fade3694a4c764504e03836bb1843321766cf2e40b7d280e84"}, + {file = "tables-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ab9291ff4d243e7966b6706a2675b83138bd9bbe82721d695b78971660d59632"}, + {file = "tables-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c14dc879b041cf53be1afe9e5ed581e1aeacdcee9e2e1ee79110dc96a4c8d97c"}, + {file = "tables-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2848fb3dce30a7b83fa099d026a91d7b10ad48afae04fa10f974f1da3f1e2bbf"}, + {file = "tables-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b131c9b4e003816a45e2efe5c5c797d01d8308cac4aee72597a15837cedb605c"}, + {file = "tables-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:c6304d321452fd56865e5c309e38373011b0f0f6c714786c5660613ceb623acb"}, + {file = "tables-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c52087ed8b90a5f6ba87f0adcd1c433e5f5db7c7ca5984b08ff45f2247635f7d"}, + {file = "tables-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:164b945d0cb731c7232775fd3657f150bcf05413928b86033b023a1dc8dbeb05"}, + {file = "tables-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a068d4ad08d5a6b2ad457f60ac6676efdab9e29459e776e433d5537a46e62e41"}, + {file = "tables-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:bca5a6bf162a84a6ef74ca4017b28c59c1526cffdbd93ce94c98ff8f9593f1d5"}, + {file = "tables-3.9.2.tar.gz", hash = "sha256:d470263c2e50c4b7c8635a0d99ac1ff2f9e704c24d71e5fa33c4529e7d0ad9c3"}, +] + +[package.dependencies] +blosc2 = ">=2.3.0" numexpr = ">=2.6.2" numpy = ">=1.19.0" packaging = "*" py-cpuinfo = "*" [package.extras] -doc = ["ipython", "numpydoc", "sphinx (>=1.1)", "sphinx-rtd-theme"] +doc = ["ipython", "numpydoc", "sphinx (>=1.1,<6)", "sphinx-rtd-theme"] [[package]] name = "terminado" @@ -5908,20 +5719,20 @@ files = [ [[package]] name = "tifffile" -version = "2023.7.10" +version = "2024.2.12" description = "Read and write TIFF files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "tifffile-2023.7.10-py3-none-any.whl", hash = "sha256:94dfdec321ace96abbfe872a66cfd824800c099a2db558443453eebc2c11b304"}, - {file = "tifffile-2023.7.10.tar.gz", hash = "sha256:c06ec460926d16796eeee249a560bcdddf243daae36ac62af3c84a953cd60b4a"}, + {file = "tifffile-2024.2.12-py3-none-any.whl", hash = "sha256:870998f82fbc94ff7c3528884c1b0ae54863504ff51dbebea431ac3fa8fb7c21"}, + {file = "tifffile-2024.2.12.tar.gz", hash = "sha256:4920a3ec8e8e003e673d3c6531863c99eedd570d1b8b7e141c072ed78ff8030d"}, ] [package.dependencies] numpy = "*" [package.extras] -all = ["defusedxml", "fsspec", "imagecodecs (>=2023.1.23)", "lxml", "matplotlib", "zarr"] +all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib", "zarr"] [[package]] name = "tinycss2" @@ -6184,7 +5995,6 @@ files = [ ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} pytz-deprecation-shim = "*" tzdata = {version = "*", markers = "platform_system == \"Windows\""} @@ -6311,25 +6121,25 @@ notebook = ">=4.4.1" [[package]] name = "xarray" -version = "2023.1.0" +version = "2024.3.0" description = "N-D labeled arrays and datasets in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "xarray-2023.1.0-py3-none-any.whl", hash = "sha256:7e530b1deafdd43e5c2b577d0944e6b528fbe88045fd849e49a8d11871ecd522"}, - {file = "xarray-2023.1.0.tar.gz", hash = "sha256:7bee552751ff1b29dab8b7715726e5ecb56691ac54593cf4881dff41978ce0cd"}, + {file = "xarray-2024.3.0-py3-none-any.whl", hash = "sha256:ca2bc4da2bf2e7879e15862a7a7c3fc76ad19f6a08931d030220cef39a29118d"}, + {file = "xarray-2024.3.0.tar.gz", hash = "sha256:5c1db19efdde61db7faedad8fc944f4e29698fb6fbd578d352668b63598bd1d8"}, ] [package.dependencies] -numpy = ">=1.20" -packaging = ">=21.3" -pandas = ">=1.3" +numpy = ">=1.23" +packaging = ">=22" +pandas = ">=1.5" [package.extras] -accel = ["bottleneck", "flox", "numbagg", "scipy"] -complete = ["bottleneck", "cfgrib", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "matplotlib", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "rasterio", "scipy", "seaborn", "zarr"] -docs = ["bottleneck", "cfgrib", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "ipykernel", "ipython", "jupyter-client", "matplotlib", "nbsphinx", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "rasterio", "scanpydoc", "scipy", "seaborn", "sphinx-autosummary-accessors", "sphinx-rtd-theme", "zarr"] -io = ["cfgrib", "cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "rasterio", "scipy", "zarr"] +accel = ["bottleneck", "flox", "numbagg", "opt-einsum", "scipy"] +complete = ["xarray[accel,dev,io,parallel,viz]"] +dev = ["hypothesis", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "xarray[complete]"] +io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] parallel = ["dask[complete]"] viz = ["matplotlib", "nc-time-axis", "seaborn"] @@ -6448,23 +6258,23 @@ test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"] [[package]] name = "zarr" -version = "2.16.1" +version = "2.17.2" description = "An implementation of chunked, compressed, N-dimensional arrays for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zarr-2.16.1-py3-none-any.whl", hash = "sha256:de4882433ccb5b42cc1ec9872b95e64ca3a13581424666b28ed265ad76c7056f"}, - {file = "zarr-2.16.1.tar.gz", hash = "sha256:4276cf4b4a653431042cd53ff2282bc4d292a6842411e88529964504fb073286"}, + {file = "zarr-2.17.2-py3-none-any.whl", hash = "sha256:70d7cc07c24280c380ef80644151d136b7503b0d83c9f214e8000ddc0f57f69b"}, + {file = "zarr-2.17.2.tar.gz", hash = "sha256:2cbaa6cb4e342d45152d4a7a4b2013c337fcd3a8e7bc98253560180de60552ce"}, ] [package.dependencies] asciitree = "*" -fasteners = "*" +fasteners = {version = "*", markers = "sys_platform != \"emscripten\""} numcodecs = ">=0.10.0" -numpy = ">=1.20,<1.21.0 || >1.21.0" +numpy = ">=1.23" [package.extras] -docs = ["numcodecs[msgpack]", "numpydoc", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx-issues", "sphinx-rtd-theme"] +docs = ["numcodecs[msgpack]", "numpydoc", "pydata-sphinx-theme", "sphinx", "sphinx-automodapi", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] jupyter = ["ipytree (>=0.2.2)", "ipywidgets (>=8.0.0)", "notebook"] [[package]] @@ -6497,5 +6307,5 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" -python-versions = ">=3.8, <3.12" -content-hash = "66c5bb92a345d6e6ec2bd10a053bec3b59cab014b524a59f63a6ea150963a12c" +python-versions = ">=3.9, <3.13" +content-hash = "f73108f3a866b54132576f726b95161bf796c126027d912e8fb0cac783c8c1cc" From 7b6062ca8f0b9f7f32987572d34cec4a5310b16f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 12:42:25 +0200 Subject: [PATCH 043/300] add dask dataframe dep --- .github/workflows/testing_coverage.yml | 2 +- poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index 515042ab..692e2ab4 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -27,7 +27,7 @@ jobs: poetry-version: 1.2.2 # Run pytest with coverage report, saving to xml - - name: Run tests on python 3.8 + - name: Run tests on python 3.9 run: | poetry run pytest --cov --cov-report xml:cobertura.xml --full-trace --show-capture=no -sv -n auto tests/ diff --git a/poetry.lock b/poetry.lock index f212450d..14ca638b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -930,10 +930,12 @@ files = [ [package.dependencies] click = ">=8.1" cloudpickle = ">=1.5.0" +dask-expr = {version = ">=1.0,<1.1", optional = true, markers = "extra == \"dataframe\""} fsspec = ">=2021.09.0" importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} numpy = {version = ">=1.21", optional = true, markers = "extra == \"array\""} packaging = ">=20.0" +pandas = {version = ">=1.3", optional = true, markers = "extra == \"dataframe\""} partd = ">=1.2.0" pyyaml = ">=5.3.1" toolz = ">=0.10.0" @@ -946,6 +948,22 @@ diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] distributed = ["distributed (==2024.4.1)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] +[[package]] +name = "dask-expr" +version = "1.0.11" +description = "High Level Expressions for Dask" +optional = false +python-versions = ">=3.9" +files = [ + {file = "dask-expr-1.0.11.tar.gz", hash = "sha256:556e13dfa081cacb0fd14b026bde18703884c12bc4659531e440507eaaee6822"}, + {file = "dask_expr-1.0.11-py3-none-any.whl", hash = "sha256:66e1f35252875fe1875ae371bc84ab4f7f6c35692835ed545d41fac61cd0759b"}, +] + +[package.dependencies] +dask = "2024.4.1" +pandas = ">=2" +pyarrow = ">=7.0.0" + [[package]] name = "debugpy" version = "1.8.1" @@ -6308,4 +6326,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "f73108f3a866b54132576f726b95161bf796c126027d912e8fb0cac783c8c1cc" +content-hash = "afc0fbd0c0231c8c0ecc67077a3cc55c3672cf201f1cbc39c81a0d77ded55049" diff --git a/pyproject.toml b/pyproject.toml index 39b8279a..94a92052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9, <3.13" bokeh = ">=2.4.2" -dask = ">=2021.12.0" +dask = {extras = ["dataframe"], version = ">=2021.12.0"} fastdtw = ">=0.3.4" fastparquet = ">=0.8.0" h5py = ">=3.6.0" From 084a6b07e808a3a339205769114798447aebb731 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 12:55:59 +0200 Subject: [PATCH 044/300] remove uncessary test file --- sed/__init__.py | 1 - tests/test_sed.py | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 tests/test_sed.py diff --git a/sed/__init__.py b/sed/__init__.py index 997765cc..b58c38f6 100644 --- a/sed/__init__.py +++ b/sed/__init__.py @@ -3,5 +3,4 @@ """ from .core.processor import SedProcessor -__version__ = "0.1.0" __all__ = ["SedProcessor"] diff --git a/tests/test_sed.py b/tests/test_sed.py deleted file mode 100644 index 0d422524..00000000 --- a/tests/test_sed.py +++ /dev/null @@ -1,8 +0,0 @@ -"""This file contains code that performs several tests for the sed package -""" -from sed import __version__ - - -def test_version() -> None: - """This function tests for the version of the package""" - assert __version__ == "0.1.0" From b49eded5e3304db44eba133ca616c34a5ac5fbdb Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 12:56:29 +0200 Subject: [PATCH 045/300] get version from toml version --- sed/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sed/__init__.py b/sed/__init__.py index b58c38f6..3abeac03 100644 --- a/sed/__init__.py +++ b/sed/__init__.py @@ -1,6 +1,10 @@ """sed module easy access APIs """ +import toml + from .core.processor import SedProcessor +config = toml.load("pyproject.toml") +__version__ = config["tool"]["poetry"]["version"] __all__ = ["SedProcessor"] From e6f3943c4d263d16cf2da594f1075f13b1be7b70 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 13:15:54 +0200 Subject: [PATCH 046/300] fix version --- sed/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sed/__init__.py b/sed/__init__.py index 3abeac03..b58c38f6 100644 --- a/sed/__init__.py +++ b/sed/__init__.py @@ -1,10 +1,6 @@ """sed module easy access APIs """ -import toml - from .core.processor import SedProcessor -config = toml.load("pyproject.toml") -__version__ = config["tool"]["poetry"]["version"] __all__ = ["SedProcessor"] From 2b4c93e1cea6fe3cb5b1d85166d74de2d7677eea Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 14:58:45 +0200 Subject: [PATCH 047/300] fix mypy error --- .pre-commit-config.yaml | 4 +- sed/calibrator/energy.py | 89 ++++++++++++++++++-------------------- sed/calibrator/momentum.py | 69 ++++++++++++++--------------- 3 files changed, 77 insertions(+), 85 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f535dab..6698791c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,12 +27,12 @@ repos: rev: v3.8.2 hooks: - id: reorder-python-imports - args: [--application-directories, '.:src', --py36-plus] + args: [--application-directories, '.:src', --py39-plus] - repo: https://github.com/asottile/pyupgrade rev: v2.37.3 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py39-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index d05e22e4..0724a3c1 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -3,16 +3,13 @@ """ import itertools as it import warnings as wn +from collections.abc import Sequence from copy import deepcopy from datetime import datetime from functools import partial from typing import Any from typing import cast -from typing import Dict -from typing import List from typing import Literal -from typing import Sequence -from typing import Tuple from typing import Union import bokeh.plotting as pbk @@ -93,9 +90,9 @@ def __init__( self._config = config - self.featranges: List[Tuple] = [] # Value ranges for feature detection + self.featranges: list[tuple] = [] # Value ranges for feature detection self.peaks: np.ndarray = np.asarray([]) - self.calibration: Dict[str, Any] = self._config["energy"].get("calibration", {}) + self.calibration: dict[str, Any] = self._config["energy"].get("calibration", {}) self.tof_column = self._config["dataframe"]["tof_column"] self.tof_ns_column = self._config["dataframe"].get("tof_ns_column", None) @@ -114,8 +111,8 @@ def __init__( self.color_clip = self._config["energy"]["color_clip"] self.sector_delays = self._config["dataframe"].get("sector_delays", None) self.sector_id_column = self._config["dataframe"].get("sector_id_column", None) - self.offsets: Dict[str, Any] = self._config["energy"].get("offsets", {}) - self.correction: Dict[str, Any] = self._config["energy"].get("correction", {}) + self.offsets: dict[str, Any] = self._config["energy"].get("offsets", {}) + self.correction: dict[str, Any] = self._config["energy"].get("correction", {}) @property def ntraces(self) -> int: @@ -177,10 +174,10 @@ def load_data( def bin_data( self, - data_files: List[str], - axes: List[str] = None, - bins: List[int] = None, - ranges: Sequence[Tuple[float, float]] = None, + data_files: list[str], + axes: list[str] = None, + bins: list[int] = None, + ranges: Sequence[tuple[float, float]] = None, biases: np.ndarray = None, bias_key: str = None, **kwds, @@ -209,7 +206,7 @@ def bin_data( ranges_ = [ np.array(self._config["energy"]["ranges"]) / 2 ** (self.binning - 1), ] - ranges = [cast(Tuple[float, float], tuple(v)) for v in ranges_] + ranges = [cast(tuple[float, float], tuple(v)) for v in ranges_] # pylint: disable=duplicate-code hist_mode = kwds.pop("hist_mode", self._config["binning"]["hist_mode"]) mode = kwds.pop("mode", self._config["binning"]["mode"]) @@ -289,7 +286,7 @@ def normalize(self, smooth: bool = False, span: int = 7, order: int = 1): def adjust_ranges( self, - ranges: Tuple, + ranges: tuple, ref_id: int = 0, traces: np.ndarray = None, peak_window: int = 7, @@ -429,7 +426,7 @@ def apply_func(apply: bool): # noqa: ARG001 def add_ranges( self, - ranges: Union[List[Tuple], Tuple], + ranges: Union[list[tuple], tuple], ref_id: int = 0, traces: np.ndarray = None, infer_others: bool = True, @@ -459,7 +456,7 @@ def add_ranges( # Infer the corresponding feature detection range of other traces by alignment if infer_others: assert isinstance(ranges, tuple) - newranges: List[Tuple] = [] + newranges: list[tuple] = [] for i in range(self.ntraces): pathcorr = find_correspondence( @@ -482,7 +479,7 @@ def add_ranges( def feature_extract( self, - ranges: List[Tuple] = None, + ranges: list[tuple] = None, traces: np.ndarray = None, peak_window: int = 7, ): @@ -609,7 +606,7 @@ def calibrate( def view( # pylint: disable=dangerous-default-value self, traces: np.ndarray, - segs: List[Tuple] = None, + segs: list[tuple] = None, peaks: np.ndarray = None, show_legend: bool = True, backend: str = "matplotlib", @@ -657,7 +654,7 @@ def view( # pylint: disable=dangerous-default-value if backend == "matplotlib": figsize = kwds.pop("figsize", (12, 4)) - fig, ax = plt.subplots(figsize=figsize) + fig_plt, ax = plt.subplots(figsize=figsize) for itr, trace in enumerate(traces): if align: ax.plot( @@ -785,7 +782,7 @@ def append_energy_axis( calibration: dict = None, verbose: bool = True, **kwds, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Calculate and append the energy axis to the events dataframe. Args: @@ -896,7 +893,7 @@ def append_tof_ns_axis( tof_column: str = None, tof_ns_column: str = None, **kwds, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Converts the time-of-flight time from steps to time in ns. Args: @@ -930,7 +927,7 @@ def append_tof_ns_axis( binning, df[tof_column].astype("float64"), ) - metadata: Dict[str, Any] = { + metadata: dict[str, Any] = { "applied": True, "binwidth": binwidth, "binning": binning, @@ -949,7 +946,7 @@ def gather_calibration_metadata(self, calibration: dict = None) -> dict: """ if calibration is None: calibration = self.calibration - metadata: Dict[Any, Any] = {} + metadata: dict[Any, Any] = {} metadata["applied"] = True metadata["calibration"] = deepcopy(calibration) metadata["tof"] = deepcopy(self.tof) @@ -964,7 +961,7 @@ def adjust_energy_correction( image: xr.DataArray, correction_type: str = None, amplitude: float = None, - center: Tuple[float, float] = None, + center: tuple[float, float] = None, correction: dict = None, apply: bool = False, **kwds, @@ -1318,7 +1315,7 @@ def apply_energy_correction( correction: dict = None, verbose: bool = True, **kwds, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Apply correction to the time-of-flight (TOF) axis of single-event data. Args: @@ -1417,7 +1414,7 @@ def gather_correction_metadata(self, correction: dict = None) -> dict: """ if correction is None: correction = self.correction - metadata: Dict[Any, Any] = {} + metadata: dict[Any, Any] = {} metadata["applied"] = True metadata["correction"] = deepcopy(correction) @@ -1429,7 +1426,7 @@ def align_dld_sectors( tof_column: str = None, sector_id_column: str = None, sector_delays: np.ndarray = None, - ) -> Tuple[dask.dataframe.DataFrame, dict]: + ) -> tuple[dask.dataframe.DataFrame, dict]: """Aligns the time-of-flight axis of the different sections of a detector. Args: @@ -1465,7 +1462,7 @@ def align_sector(x): return val.astype(np.float32) df[tof_column] = df.map_partitions(align_sector, meta=(tof_column, np.float32)) - metadata: Dict[str, Any] = { + metadata: dict[str, Any] = { "applied": True, "sector_delays": sector_delays, } @@ -1474,7 +1471,7 @@ def align_sector(x): def add_offsets( self, df: Union[pd.DataFrame, dask.dataframe.DataFrame] = None, - offsets: Dict[str, Any] = None, + offsets: dict[str, Any] = None, constant: float = None, columns: Union[str, Sequence[str]] = None, weights: Union[float, Sequence[float]] = None, @@ -1482,7 +1479,7 @@ def add_offsets( reductions: Union[str, Sequence[str]] = None, energy_column: str = None, verbose: bool = True, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Apply an offset to the energy column by the values of the provided columns. If no parameter is passed to this function, the offset is applied as defined in the @@ -1516,7 +1513,7 @@ def add_offsets( if energy_column is None: energy_column = self.energy_column - metadata: Dict[str, Any] = { + metadata: dict[str, Any] = { "applied": True, } @@ -1644,7 +1641,7 @@ def add_offsets( return df, metadata -def extract_bias(files: List[str], bias_key: str) -> np.ndarray: +def extract_bias(files: list[str], bias_key: str) -> np.ndarray: """Read bias values from hdf5 files Args: @@ -1654,7 +1651,7 @@ def extract_bias(files: List[str], bias_key: str) -> np.ndarray: Returns: np.ndarray: Array of bias values. """ - bias_list: List[float] = [] + bias_list: list[float] = [] for file in files: with h5py.File(file, "r") as file_handle: if bias_key[0] == "@": @@ -1669,7 +1666,7 @@ def correction_function( x: Union[float, np.ndarray], y: Union[float, np.ndarray], correction_type: str, - center: Tuple[float, float], + center: tuple[float, float], amplitude: float, **kwds, ) -> Union[float, np.ndarray]: @@ -1846,9 +1843,9 @@ def find_correspondence( def range_convert( x: np.ndarray, - xrng: Tuple, + xrng: tuple, pathcorr: np.ndarray, -) -> Tuple: +) -> tuple: """Convert value range using a pairwise path correspondence (e.g. obtained from time warping algorithm). @@ -1890,7 +1887,7 @@ def find_nearest(val: float, narray: np.ndarray) -> int: def peaksearch( traces: np.ndarray, tof: np.ndarray, - ranges: List[Tuple] = None, + ranges: list[tuple] = None, pkwindow: int = 3, plot: bool = False, ) -> np.ndarray: @@ -1938,7 +1935,7 @@ def peaksearch( def _datacheck_peakdetect( x_axis: np.ndarray, y_axis: np.ndarray, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Input format checking for 1D peakdtect algorithm Args: @@ -1972,7 +1969,7 @@ def peakdetect1d( x_axis: np.ndarray = None, lookahead: int = 200, delta: int = 0, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Function for detecting local maxima and minima in a signal. Discovers peaks by searching for values which are surrounded by lower or larger values for maxima and minima respectively @@ -2086,13 +2083,13 @@ def peakdetect1d( def fit_energy_calibration( - pos: Union[List[float], np.ndarray], - vals: Union[List[float], np.ndarray], + pos: Union[list[float], np.ndarray], + vals: Union[list[float], np.ndarray], binwidth: float, binning: int, ref_id: int = 0, ref_energy: float = None, - t: Union[List[float], np.ndarray] = None, + t: Union[list[float], np.ndarray] = None, energy_scale: str = "kinetic", verbose: bool = True, **kwds, @@ -2222,12 +2219,12 @@ def residual(pars, time, data, binwidth, binning, energy_scale): def poly_energy_calibration( - pos: Union[List[float], np.ndarray], - vals: Union[List[float], np.ndarray], + pos: Union[list[float], np.ndarray], + vals: Union[list[float], np.ndarray], order: int = 3, ref_id: int = 0, ref_energy: float = None, - t: Union[List[float], np.ndarray] = None, + t: Union[list[float], np.ndarray] = None, aug: int = 1, method: str = "lstsq", energy_scale: str = "kinetic", @@ -2373,7 +2370,7 @@ def tof2ev( def tof2evpoly( - poly_a: Union[List[float], np.ndarray], + poly_a: Union[list[float], np.ndarray], energy_offset: float, t: float, ) -> float: diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index fea591cb..19c8d56d 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -5,9 +5,6 @@ from copy import deepcopy from datetime import datetime from typing import Any -from typing import Dict -from typing import List -from typing import Tuple from typing import Union import bokeh.palettes as bp @@ -51,7 +48,7 @@ class MomentumCorrector: def __init__( self, data: Union[xr.DataArray, np.ndarray] = None, - bin_ranges: List[Tuple] = None, + bin_ranges: list[tuple] = None, rotsym: int = 6, config: dict = None, ): @@ -75,7 +72,7 @@ def __init__( self.slice: np.ndarray = None self.slice_corrected: np.ndarray = None self.slice_transformed: np.ndarray = None - self.bin_ranges: List[Tuple] = self._config["momentum"].get("bin_ranges", []) + self.bin_ranges: list[tuple] = self._config["momentum"].get("bin_ranges", []) if data is not None: self.load_data(data=data, bin_ranges=bin_ranges) @@ -90,7 +87,7 @@ def __init__( self.include_center: bool = False self.use_center: bool = False self.pouter: np.ndarray = None - self.pcent: Tuple[float, ...] = None + self.pcent: tuple[float, ...] = None self.pouter_ord: np.ndarray = None self.prefs: np.ndarray = None self.ptargs: np.ndarray = None @@ -106,10 +103,10 @@ def __init__( self.cdeform_field_bkp: np.ndarray = None self.inverse_dfield: np.ndarray = None self.dfield_updated: bool = False - self.transformations: Dict[str, Any] = self._config["momentum"].get("transformations", {}) - self.correction: Dict[str, Any] = self._config["momentum"].get("correction", {}) - self.adjust_params: Dict[str, Any] = {} - self.calibration: Dict[str, Any] = self._config["momentum"].get("calibration", {}) + self.transformations: dict[str, Any] = self._config["momentum"].get("transformations", {}) + self.correction: dict[str, Any] = self._config["momentum"].get("correction", {}) + self.adjust_params: dict[str, Any] = {} + self.calibration: dict[str, Any] = self._config["momentum"].get("calibration", {}) self.x_column = self._config["dataframe"]["x_column"] self.y_column = self._config["dataframe"]["y_column"] @@ -154,7 +151,7 @@ def symscores(self) -> dict: def load_data( self, data: Union[xr.DataArray, np.ndarray], - bin_ranges: List[Tuple] = None, + bin_ranges: list[tuple] = None, ): """Load binned data into the momentum calibrator class @@ -267,9 +264,7 @@ def apply_fun(apply: bool): # noqa: ARG001 axmax = np.max(self.slice, axis=(0, 1)) if axmin < axmax: img.set_clim(axmin, axmax) - ax.set_title( - f"Plane[{start}:{stop}]", - ) + ax.set_title(f"Plane[{start}:{stop}]") fig.canvas.draw_idle() plane_slider.close() @@ -287,7 +282,7 @@ def apply_fun(apply: bool): # noqa: ARG001 def select_slice( self, - selector: Union[slice, List[int], int], + selector: Union[slice, list[int], int], axis: int = 2, ): """Select (hyper)slice from a (hyper)volume. @@ -1028,7 +1023,7 @@ def coordinate_transform( def pose_adjustment( self, - transformations: Dict[str, Any] = None, + transformations: dict[str, Any] = None, apply: bool = False, reset: bool = True, verbose: bool = True, @@ -1262,7 +1257,7 @@ def view( # pylint: disable=dangerous-default-value image: np.ndarray = None, origin: str = "lower", cmap: str = "terrain_r", - figsize: Tuple[int, int] = (4, 4), + figsize: tuple[int, int] = (4, 4), points: dict = None, annotated: bool = False, backend: str = "matplotlib", @@ -1270,7 +1265,7 @@ def view( # pylint: disable=dangerous-default-value scatterkwds: dict = {}, cross: bool = False, crosshair: bool = False, - crosshair_radii: List[int] = [50, 100, 150], + crosshair_radii: list[int] = [50, 100, 150], crosshair_thickness: int = 1, **kwds, ): @@ -1317,7 +1312,7 @@ def view( # pylint: disable=dangerous-default-value txtsize = kwds.pop("textsize", 12) if backend == "matplotlib": - fig, ax = plt.subplots(figsize=figsize) + fig_plt, ax = plt.subplots(figsize=figsize) ax.imshow(image.T, origin=origin, cmap=cmap, **imkwds) if cross: @@ -1409,11 +1404,11 @@ def view( # pylint: disable=dangerous-default-value def select_k_range( self, - point_a: Union[np.ndarray, List[int]] = None, - point_b: Union[np.ndarray, List[int]] = None, + point_a: Union[np.ndarray, list[int]] = None, + point_b: Union[np.ndarray, list[int]] = None, k_distance: float = None, - k_coord_a: Union[np.ndarray, List[float]] = None, - k_coord_b: Union[np.ndarray, List[float]] = np.array([0.0, 0.0]), + k_coord_a: Union[np.ndarray, list[float]] = None, + k_coord_b: Union[np.ndarray, list[float]] = np.array([0.0, 0.0]), equiscale: bool = True, apply: bool = False, ): @@ -1560,11 +1555,11 @@ def apply_func(apply: bool): # noqa: ARG001 def calibrate( self, - point_a: Union[np.ndarray, List[int]], - point_b: Union[np.ndarray, List[int]], + point_a: Union[np.ndarray, list[int]], + point_b: Union[np.ndarray, list[int]], k_distance: float = None, - k_coord_a: Union[np.ndarray, List[float]] = None, - k_coord_b: Union[np.ndarray, List[float]] = np.array([0.0, 0.0]), + k_coord_a: Union[np.ndarray, list[float]] = None, + k_coord_b: Union[np.ndarray, list[float]] = np.array([0.0, 0.0]), equiscale: bool = True, image: np.ndarray = None, ) -> dict: @@ -1684,7 +1679,7 @@ def apply_corrections( new_y_column: str = None, verbose: bool = True, **kwds, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Calculate and replace the X and Y values with their distortion-corrected version. @@ -1766,7 +1761,7 @@ def gather_correction_metadata(self) -> dict: Returns: dict: generated correction metadata dictionary. """ - metadata: Dict[Any, Any] = {} + metadata: dict[Any, Any] = {} if len(self.correction) > 0: metadata["correction"] = self.correction metadata["correction"]["applied"] = True @@ -1843,7 +1838,7 @@ def append_k_axis( new_y_column: str = None, calibration: dict = None, **kwds, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: """Calculate and append the k axis coordinates (kx, ky) to the events dataframe. Args: @@ -1928,7 +1923,7 @@ def gather_calibration_metadata(self, calibration: dict = None) -> dict: """ if calibration is None: calibration = self.calibration - metadata: Dict[Any, Any] = {} + metadata: dict[Any, Any] = {} try: metadata["creation_date"] = calibration["creation_date"] except KeyError: @@ -1967,7 +1962,7 @@ def cm2palette(cmap_name: str) -> list: def dictmerge( main_dict: dict, - other_entries: Union[List[dict], Tuple[dict], dict], + other_entries: Union[list[dict], tuple[dict], dict], ) -> dict: """Merge a dictionary with other dictionaries. @@ -2006,7 +2001,7 @@ def detector_coordiantes_2_k_koordinates( c_conversion: float, r_step: float, c_step: float, -) -> Tuple[float, float]: +) -> tuple[float, float]: """Conversion from detector coordinates (rdet, cdet) to momentum coordinates (kr, kc). @@ -2040,7 +2035,7 @@ def apply_dfield( y_column: str, new_x_column: str, new_y_column: str, - detector_ranges: List[Tuple], + detector_ranges: list[tuple], ) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: """Application of the inverse displacement-field to the dataframe coordinates. @@ -2075,8 +2070,8 @@ def apply_dfield( def generate_inverse_dfield( rdeform_field: np.ndarray, cdeform_field: np.ndarray, - bin_ranges: List[Tuple], - detector_ranges: List[Tuple], + bin_ranges: list[tuple], + detector_ranges: list[tuple], ) -> np.ndarray: """Generate inverse deformation field using inperpolation with griddata. Assuming the binning range of the input ``rdeform_field`` and ``cdeform_field`` @@ -2147,7 +2142,7 @@ def generate_inverse_dfield( return inverse_dfield -def load_dfield(file: str) -> Tuple[np.ndarray, np.ndarray]: +def load_dfield(file: str) -> tuple[np.ndarray, np.ndarray]: """Load inverse dfield from file Args: From e9fa5f2fae0492950e8a8cd41d2e1360bb4cba67 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 15:02:10 +0200 Subject: [PATCH 048/300] fix old pre commit file --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6698791c..7f535dab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,12 +27,12 @@ repos: rev: v3.8.2 hooks: - id: reorder-python-imports - args: [--application-directories, '.:src', --py39-plus] + args: [--application-directories, '.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade rev: v2.37.3 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py36-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: From 65e8e098d094fd4ac5978c0845af8eacc985c41f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 15:16:23 +0200 Subject: [PATCH 049/300] fix tests --- tests/calibrator/test_energy.py | 2 ++ tests/test_dfops.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 0c1735fb..49d7cc47 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -616,6 +616,7 @@ def test_add_offsets_functionality(energy_scale: str) -> None: loader=get_loader("flash", config=config), ) res, meta = ec.add_offsets(t_df) + res = res.compute() exp_vals = df["energy"].copy() + 1 * scale_sign exp_vals += (df["off1"] - df["off1"].mean()) * scale_sign exp_vals -= df["off2"] * scale_sign @@ -646,6 +647,7 @@ def test_add_offsets_functionality(energy_scale: str) -> None: res, meta = ec.add_offsets(t_df, weights=-1, columns="off1") res, meta = ec.add_offsets(res, columns="off1") exp_vals = df["energy"].copy() + res = res.compute() np.testing.assert_allclose(res["energy"].values, exp_vals.values) exp_meta = {} exp_meta["applied"] = True diff --git a/tests/test_dfops.py b/tests/test_dfops.py index 2ce894dc..0ebe794c 100644 --- a/tests/test_dfops.py +++ b/tests/test_dfops.py @@ -343,6 +343,7 @@ def test_offset_by_other_columns_functionality() -> None: offset_columns=["off1"], weights=[1], ) + res = res.compute() expected: List[Any] = [11, 22, 33, 44, 55, 66] np.testing.assert_allclose(res["target"].values, expected) @@ -352,6 +353,7 @@ def test_offset_by_other_columns_functionality() -> None: offset_columns=["off1", "off2"], weights=[1, -1], ) + res = res.compute() expected = [10.9, 21.8, 32.7, 43.6, 54.5, 65.4] np.testing.assert_allclose(res["target"].values, expected) From 04a580a66c943f1a4a858d2cc42991f9a036aff9 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 11 Apr 2024 15:23:46 +0200 Subject: [PATCH 050/300] fix tests --- tests/calibrator/test_energy.py | 3 ++- tests/test_dfops.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 49d7cc47..bd9c3705 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -633,6 +633,7 @@ def test_add_offsets_functionality(energy_scale: str) -> None: ) t_df = dask.dataframe.from_pandas(df.copy(), npartitions=2) res, meta = ec.add_offsets(t_df, **params) # type: ignore + res = res.compute() np.testing.assert_allclose(res["energy"].values, exp_vals.values) exp_meta = {} exp_meta["applied"] = True @@ -646,8 +647,8 @@ def test_add_offsets_functionality(energy_scale: str) -> None: t_df = dask.dataframe.from_pandas(df.copy(), npartitions=2) res, meta = ec.add_offsets(t_df, weights=-1, columns="off1") res, meta = ec.add_offsets(res, columns="off1") - exp_vals = df["energy"].copy() res = res.compute() + exp_vals = df["energy"].copy() np.testing.assert_allclose(res["energy"].values, exp_vals.values) exp_meta = {} exp_meta["applied"] = True diff --git a/tests/test_dfops.py b/tests/test_dfops.py index 0ebe794c..7ae888f4 100644 --- a/tests/test_dfops.py +++ b/tests/test_dfops.py @@ -364,6 +364,7 @@ def test_offset_by_other_columns_functionality() -> None: weights=[1], preserve_mean=True, ) + res = res.compute() expected = [9.75, 19.85, 29.95, 40.05, 50.15, 60.25] np.testing.assert_allclose(res["target"].values, expected) @@ -374,6 +375,7 @@ def test_offset_by_other_columns_functionality() -> None: weights=[1], reductions="mean", ) + res = res.compute() expected = [20, 30, 40, 50, 60, 70] np.testing.assert_allclose(res["target"].values, expected) From d316137f6c71941611413b711b444a4becccd3f4 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 27 Apr 2024 23:37:31 +0200 Subject: [PATCH 051/300] lint fix --- tests/loader/flash/test_dataframe_creator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index e287713f..b9c1b834 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -238,7 +238,6 @@ def test_group_name_not_in_h5(config_dataframe, h5_file): channel = "dldPosX" config = config_dataframe config["channels"][channel]["group_name"] = "foo" - index_key = "foo" + "index" df = DataFrameCreator(config, h5_file) with pytest.raises(KeyError): From c00207d3c347e4dd9774df190c36b699446be327 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 28 Apr 2024 00:41:05 +0200 Subject: [PATCH 052/300] update dataframe saving and loading from parquet behavior --- sed/loader/flash/loader.py | 163 ++++----------------- sed/loader/flash/utils.py | 43 ++++++ tests/loader/flash/test_flash_loader.py | 14 +- tests/loader/flash/test_parquet_handler.py | 71 ++------- 4 files changed, 88 insertions(+), 203 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index df8f8a58..40573279 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -32,6 +32,7 @@ from sed.loader.base.loader import BaseLoader from sed.loader.flash.metadata import MetadataRetriever from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import initialize_parquet_paths from sed.loader.utils import split_dld_time_from_sector_id @@ -55,9 +56,11 @@ def __init__(self, config: dict) -> None: """ super().__init__(config=config) - def initialize_paths(self) -> tuple[list[Path], Path]: + def initialize_dir(self) -> tuple[list[Path], Path]: """ - Initializes the paths based on the configuration. + Initializes the directories based on the configuration. If paths is provided in the + configuration, the raw data directories and parquet data directory are taken from there. + Otherwise, the beamtime_id and year are used to locate the data directories. Returns: Tuple[List[Path], Path]: A tuple containing a list of raw data directories @@ -199,6 +202,17 @@ def get_count_rate( def get_elapsed_time(self, fids=None, **kwds): # noqa: ARG002 return None + def read_parquet(self, parquet_paths: Sequence[Path] = None): + dfs = [] + for parquet_path in parquet_paths: + if not parquet_path.exists(): + raise FileNotFoundError( + f"The Parquet file at {parquet_path} does not exist. ", + "If it is in another location, provide the correct path as parquet_path.", + ) + dfs.append(dd.read_parquet(parquet_path)) + return dfs + def read_dataframe( self, files: str | Sequence[str] = None, @@ -241,7 +255,7 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self.initialize_paths() + data_raw_dir, data_parquet_dir = self.initialize_dir() # Prepare a list of names for the runs to read and parquets to write if runs is not None: @@ -271,13 +285,14 @@ def read_dataframe( # if parquet_dir is None, use data_parquet_dir parquet_dir = parquet_dir or data_parquet_dir - parquet_path = Path(parquet_dir) + parquet_dir = Path(parquet_dir) filename = "_".join(str(run) for run in self.runs) converted_str = "converted" if converted else "" + # Create parquet paths for saving and loading the parquet files of df and timed_df - ph = ParquetHandler( + parquet_paths = initialize_parquet_paths( parquet_names=[filename, filename + "_timed"], - folder=parquet_path, + folder=parquet_dir, subfolder=converted_str, prefix="run_", suffix=detector, @@ -285,9 +300,7 @@ def read_dataframe( # Check if load_parquet is flagged and then load the file if it exists if load_parquet: - df_list = ph.read_parquet() - df = df_list[0] - df_timed = df_list[1] + df, df_timed = self.read_parquet(parquet_paths) # Default behavior is to create the buffer files and load them else: @@ -299,7 +312,7 @@ def read_dataframe( ) buffer.run( h5_paths=h5_paths, - folder=parquet_path, + folder=parquet_dir, force_recreate=force_recreate, suffix=detector, debug=debug, @@ -309,7 +322,8 @@ def read_dataframe( # Save the dataframe as parquet if requested if save_parquet: - ph.save_parquet([df, df_timed], drop_index=True) + df.compute().reset_index(drop=True).to_parquet(parquet_paths[0]) + df_timed.compute().reset_index(drop=True).to_parquet(parquet_paths[1]) metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") @@ -654,7 +668,7 @@ def get_files_to_read( force_recreate (bool): Flag to force recreation of buffer files. """ # Getting the paths of the buffer files, with subfolder as buffer and no extension - ph = ParquetHandler( + self.buffer_paths = initialize_parquet_paths( parquet_names=[Path(h5_path).stem for h5_path in h5_paths], folder=folder, subfolder="buffer", @@ -662,8 +676,6 @@ def get_files_to_read( suffix=suffix, extension="", ) - ph.initialize_paths() - self.buffer_paths = ph.parquet_paths # read only the files that do not exist or if force_recreate is True files_to_read = [ force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths @@ -796,127 +808,4 @@ def run( self.fill_dataframes() -class ParquetHandler: - """A class for handling the creation and manipulation of Parquet files.""" - - def __init__( - self, - parquet_names: str | list[str] = None, - folder: Path = None, - subfolder: str = "", - prefix: str = "", - suffix: str = "", - extension: str = "parquet", - ): - """ - Initialize the ParquetHandler. - - Args: - parquet_names Union[str, List[str]]: The base name of the Parquet files. - folder (Path): The directory where the Parquet file will be stored. - subfolder (str): Optional subfolder within the main folder. - prefix (str): Optional prefix for the Parquet file name. - suffix (str): Optional suffix for the Parquet file name. - extension (str): Optional extension for the Parquet file names. - """ - self.parquet_paths: list[Path] = None - - if isinstance(parquet_names, str): - parquet_names = [parquet_names] - - self.parquet_names = parquet_names - self.folder = folder - self.subfolder = subfolder - self.prefix = prefix - self.suffix = suffix - self.extension = extension - - def initialize_paths( - self, - parquet_paths: list[Path] = None, - ) -> None: - """ - Initialize the paths for the Parquet files. - - If custom paths are provided, they will be used. Otherwise, paths will be generated based on - the specified parameters during initialization. - - Args: - parquet_paths (List[Path]): Optional custom paths for the Parquet files. - """ - # If parquet_paths is provided, use it and return - if parquet_paths: - self.parquet_paths = parquet_paths - return - - if self.parquet_paths: - return - - if not self.folder and not parquet_paths: - raise ValueError("Please provide folder or parquet_paths.") - if self.folder and not self.parquet_names: - raise ValueError("With folder, please provide parquet_names.") - - # Otherwise create the full path for the Parquet file - parquet_dir = self.folder.joinpath(self.subfolder) - parquet_dir.mkdir(parents=True, exist_ok=True) - - if self.extension: - self.extension = f".{self.extension}" # to be backwards compatible - self.parquet_paths = [ - parquet_dir.joinpath(Path(f"{self.prefix}{name}{self.suffix}{self.extension}")) - for name in self.parquet_names - ] - - def save_parquet( - self, - dfs: list[dd.DataFrame], - parquet_paths: list[Path] = None, - drop_index: bool = False, - ) -> None: - """ - Save the Dask DataFrames to Parquet files. - - Args: - dfs (list[dd.DataFrame]): The list of Dask DataFrames to be saved. - parquet_paths (List[Path]): Optional custom paths for the Parquet files. - drop_index (bool): If True, drops the index before saving. - """ - self.initialize_paths(parquet_paths) - - if len(dfs) != len(self.parquet_paths): - raise ValueError("Number of DataFrames provided does not match the number of paths.") - - # Compute the Dask DataFrame, reset the index, and save to Parquet - for df, parquet_path in zip(dfs, self.parquet_paths): - df.compute().reset_index(drop=drop_index).to_parquet(parquet_path) - - def read_parquet( - self, - parquet_paths: list[Path] = None, - ) -> list[dd.DataFrame]: - """ - Read Dask DataFrames from Parquet files. - - Args: - parquet_paths (List[Path]): Optional custom paths for the Parquet files. - - Returns: - List[dd.DataFrame]: The list of Dask DataFrames read from the Parquet files. - - Raises: - FileNotFoundError: If any of the specified Parquet files do not exist. - """ - self.initialize_paths(parquet_paths) - dfs = [] - for parquet_path in self.parquet_paths: - if not parquet_path.exists(): - raise FileNotFoundError( - "The Parquet file does not exist. " - "If it is in another location, provide the correct path as parquet_path.", - ) - dfs.append(dd.read_parquet(parquet_path)) - return dfs - - LOADER = FlashLoader diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index a75c3d98..764f64cd 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -1,5 +1,7 @@ from __future__ import annotations +from pathlib import Path + MULTI_INDEX = ["trainId", "pulseId", "electronId"] PULSE_ALIAS = MULTI_INDEX[1] DLD_AUX_ALIAS = "dldAux" @@ -78,3 +80,44 @@ def get_channels( channels.extend([DLD_AUX_ALIAS]) return channels + + +def initialize_parquet_paths( + parquet_names: str | list[str] = None, + folder: Path = None, + subfolder: str = "", + prefix: str = "", + suffix: str = "", + extension: str = "parquet", + parquet_paths: list[Path] = None, +) -> list[Path]: + """ + Initialize the paths for the Parquet files. + + If custom paths are provided, they will be used. Otherwise, paths will be generated based on + the specified parameters during initialization. + + Args: + parquet_paths (List[Path]): Optional custom paths for the Parquet files. + """ + # if parquet_names is string, convert it to a list + if isinstance(parquet_names, str): + parquet_names = [parquet_names] + + # Check if the folder and Parquet paths are provided + if not folder and not parquet_paths: + raise ValueError("Please provide folder or parquet_paths.") + if folder and not parquet_names: + raise ValueError("With folder, please provide parquet_names.") + + # Otherwise create the full path for the Parquet file + parquet_dir = folder.joinpath(subfolder) + parquet_dir.mkdir(parents=True, exist_ok=True) + + if extension: + extension = f".{extension}" # to be backwards compatible + parquet_paths = [ + parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) for name in parquet_names + ] + + return parquet_paths diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 096e9454..c466316d 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -29,7 +29,7 @@ def fixture_config_file() -> dict: "sub_dir", ["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], ) -def test_initialize_paths( +def test_initialize_dir( config_file: dict, fs, sub_dir: Literal["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], @@ -59,9 +59,9 @@ def test_initialize_paths( fs.create_dir(expected_raw_path) fs.create_dir(expected_processed_path) - # Instance of class with correct config and call initialize_paths + # Instance of class with correct config and call initialize_dir fl = FlashLoader(config=config) - data_raw_dir, data_parquet_dir = fl.initialize_paths() + data_raw_dir, data_parquet_dir = fl.initialize_dir() assert expected_raw_path == data_raw_dir[0] assert expected_processed_path == data_parquet_dir @@ -70,12 +70,12 @@ def test_initialize_paths( del config["core"]["beamtime_id"] fl = FlashLoader(config=config) with pytest.raises(ValueError) as e: - _, _ = fl.initialize_paths() + _, _ = fl.initialize_dir() assert "The beamtime_id, year and daq are required." in str(e.value) -def test_initialize_paths_filenotfound(config_file: dict) -> None: +def test_initialize_dir_filenotfound(config_file: dict) -> None: """ Test FileNotFoundError during the initialization of paths. """ @@ -85,10 +85,10 @@ def test_initialize_paths_filenotfound(config_file: dict) -> None: config["core"]["beamtime_id"] = "11111111" config["core"]["year"] = "2000" - # Instance of class with correct config and call initialize_paths + # Instance of class with correct config and call initialize_dir fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = fl.initialize_paths() + _, _ = fl.initialize_dir() def test_save_read_parquet_flash(config): diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index e241d491..db7d084f 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -1,10 +1,8 @@ from pathlib import Path -import numpy as np import pytest -from sed.loader.flash.loader import BufferHandler -from sed.loader.flash.loader import ParquetHandler +from sed.loader.flash.utils import initialize_parquet_paths def create_parquet_dir(config, folder): @@ -17,14 +15,12 @@ def create_parquet_dir(config, folder): def test_parquet_init_error(): """Test ParquetHandler initialization error""" with pytest.raises(ValueError) as e: - ph = ParquetHandler(parquet_names="test") - ph.initialize_paths() + _ = initialize_parquet_paths(parquet_names="test") assert "Please provide folder or parquet_paths." in str(e.value) with pytest.raises(ValueError) as e: - ph = ParquetHandler(folder="test") - ph.initialize_paths() + _ = initialize_parquet_paths(folder="test") assert "With folder, please provide parquet_names." in str(e.value) @@ -33,59 +29,16 @@ def test_initialize_paths(config): """Test ParquetHandler initialization""" folder = create_parquet_dir(config, "parquet_init") - ph = ParquetHandler("test", folder, extension="xyz") - ph.initialize_paths() - assert ph.parquet_paths[0].suffix == ".xyz" - assert ph.parquet_paths[0].name == "test.xyz" + ph = initialize_parquet_paths("test", folder, extension="xyz") + assert ph[0].suffix == ".xyz" + assert ph[0].name == "test.xyz" # test prefix and suffix - ph = ParquetHandler("test", folder, prefix="prefix_", suffix="_suffix") - ph.initialize_paths() - assert ph.parquet_paths[0].name == "prefix_test_suffix.parquet" + ph = initialize_parquet_paths("test", folder, prefix="prefix_", suffix="_suffix") + assert ph[0].name == "prefix_test_suffix.parquet" # test with list of parquet_names and subfolder - ph = ParquetHandler(["test1", "test2"], folder, subfolder="subfolder") - ph.initialize_paths() - assert ph.parquet_paths[0].parent.name == "subfolder" - assert ph.parquet_paths[0].name == "test1.parquet" - assert ph.parquet_paths[1].name == "test2.parquet" - - -def test_save_read_parquet(config, h5_paths): - """Test ParquetHandler save and read parquet""" - # provide instead parquet_paths - folder = create_parquet_dir(config, "parquet_save") - parquet_path = folder.joinpath("test.parquet") - - # create some parquet files for testing save and read - ph = ParquetHandler() - bh = BufferHandler(config["dataframe"]) - bh.run(h5_paths=h5_paths, folder=folder) - - ph.save_parquet([bh.dataframe_electron], parquet_paths=[parquet_path], drop_index=False) - parquet_path.unlink() # remove parquet file - ph.save_parquet([bh.dataframe_electron], parquet_paths=[parquet_path], drop_index=True) - - # Provide different number of DataFrames and paths - with pytest.raises(ValueError) as e: - ph.save_parquet( - [bh.dataframe_electron], - parquet_paths=[parquet_path, parquet_path], - drop_index=False, - ) - - assert "Number of DataFrames provided does not match the number of paths." in str(e.value) - - df = ph.read_parquet() - df_loaded = df[0].compute() - df_saved = bh.dataframe_electron.compute().reset_index(drop=True) - - # compare the saved and loaded dataframes - assert np.all(df[0].columns == bh.dataframe_electron.columns) - assert df_loaded.equals(df_saved) - - [path.unlink() for path in bh.buffer_paths] # remove buffer files - parquet_path.unlink() # remove parquet file - # Test file not found - with pytest.raises(FileNotFoundError) as e: - ph.read_parquet() + ph = initialize_parquet_paths(["test1", "test2"], folder, subfolder="subfolder") + assert ph[0].parent.name == "subfolder" + assert ph[0].name == "test1.parquet" + assert ph[1].name == "test2.parquet" From 89130b0296cf3ef2bcbc645b3200119043ee66b8 Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 20:10:22 +0200 Subject: [PATCH 053/300] remove saving/loading of parquets --- sed/loader/flash/loader.py | 125 ++++++++++++------------------------- sed/loader/flash/utils.py | 42 +++++++------ 2 files changed, 62 insertions(+), 105 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 40573279..266216ab 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -32,7 +32,7 @@ from sed.loader.base.loader import BaseLoader from sed.loader.flash.metadata import MetadataRetriever from sed.loader.flash.utils import get_channels -from sed.loader.flash.utils import initialize_parquet_paths +from sed.loader.flash.utils import initialize_paths from sed.loader.utils import split_dld_time_from_sector_id @@ -56,10 +56,10 @@ def __init__(self, config: dict) -> None: """ super().__init__(config=config) - def initialize_dir(self) -> tuple[list[Path], Path]: + def initialize_dirs(self) -> tuple[list[Path], Path]: """ - Initializes the directories based on the configuration. If paths is provided in the - configuration, the raw data directories and parquet data directory are taken from there. + Initializes the directories on Maxwell based on configuration. If paths is provided in + the configuration, the raw data directories and parquet data directory are taken from there. Otherwise, the beamtime_id and year are used to locate the data directories. Returns: @@ -202,17 +202,6 @@ def get_count_rate( def get_elapsed_time(self, fids=None, **kwds): # noqa: ARG002 return None - def read_parquet(self, parquet_paths: Sequence[Path] = None): - dfs = [] - for parquet_path in parquet_paths: - if not parquet_path.exists(): - raise FileNotFoundError( - f"The Parquet file at {parquet_path} does not exist. ", - "If it is in another location, provide the correct path as parquet_path.", - ) - dfs.append(dd.read_parquet(parquet_path)) - return dfs - def read_dataframe( self, files: str | Sequence[str] = None, @@ -221,9 +210,6 @@ def read_dataframe( ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, - converted: bool = False, - load_parquet: bool = False, - save_parquet: bool = False, detector: str = "", force_recreate: bool = False, parquet_dir: str | Path = None, @@ -255,11 +241,12 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self.initialize_dir() + data_raw_dir, data_parquet_dir = self.initialize_dirs() # Prepare a list of names for the runs to read and parquets to write if runs is not None: files = [] + run_files_dict = {} # Useful for saving entire runs if isinstance(runs, (str, int)): runs = [runs] for run in runs: @@ -269,6 +256,7 @@ def read_dataframe( extension=ftype, daq=self._config["dataframe"]["daq"], ) + run_files_dict[str(run)] = run_files files.extend(run_files) self.runs = list(runs) super().read_dataframe(files=files, ftype=ftype) @@ -283,47 +271,26 @@ def read_dataframe( metadata=metadata, ) + bh = BufferHandler( + config=self._config, + ) + # if parquet_dir is None, use data_parquet_dir parquet_dir = parquet_dir or data_parquet_dir parquet_dir = Path(parquet_dir) - filename = "_".join(str(run) for run in self.runs) - converted_str = "converted" if converted else "" - # Create parquet paths for saving and loading the parquet files of df and timed_df - parquet_paths = initialize_parquet_paths( - parquet_names=[filename, filename + "_timed"], + # Obtain the parquet filenames, metadata, and schema from the method + # which handles buffer file creation/reading + h5_paths = [Path(file) for file in self.files] + bh.run( + h5_paths=h5_paths, folder=parquet_dir, - subfolder=converted_str, - prefix="run_", + force_recreate=force_recreate, suffix=detector, + debug=debug, ) - - # Check if load_parquet is flagged and then load the file if it exists - if load_parquet: - df, df_timed = self.read_parquet(parquet_paths) - - # Default behavior is to create the buffer files and load them - else: - # Obtain the parquet filenames, metadata, and schema from the method - # which handles buffer file creation/reading - h5_paths = [Path(file) for file in self.files] - buffer = BufferHandler( - config_dataframe=self._config["dataframe"], - ) - buffer.run( - h5_paths=h5_paths, - folder=parquet_dir, - force_recreate=force_recreate, - suffix=detector, - debug=debug, - ) - df = buffer.dataframe_electron - df_timed = buffer.dataframe_pulse - - # Save the dataframe as parquet if requested - if save_parquet: - df.compute().reset_index(drop=True).to_parquet(parquet_paths[0]) - df_timed.compute().reset_index(drop=True).to_parquet(parquet_paths[1]) + df = bh.dataframe_electron + df_timed = bh.dataframe_pulse metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") @@ -586,27 +553,21 @@ def df(self) -> DataFrame: class BufferHandler: """ - A class for handling the creation and manipulation of buffer files using DataFrameCreator - and ParquetHandler. + A class for handling the creation and manipulation of buffer files using DataFrameCreator. """ def __init__( self, - config_dataframe: dict, + config: dict, ) -> None: """ - Initializes the BufferFileHandler. + Initializes the BufferHandler. Args: - config_dataframe (dict): The configuration dictionary with only the dataframe key. - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - force_recreate (bool): Flag to force recreation of buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - debug (bool): Flag to enable debug mode. + config (dict): The configuration dictionary. """ - self._config = config_dataframe + self._config = config["dataframe"] + self.n_cores = config["core"].get("num_cores", os.cpu_count() - 1) self.buffer_paths: list[Path] = [] self.missing_h5_files: list[Path] = [] @@ -668,8 +629,8 @@ def get_files_to_read( force_recreate (bool): Flag to force recreation of buffer files. """ # Getting the paths of the buffer files, with subfolder as buffer and no extension - self.buffer_paths = initialize_parquet_paths( - parquet_names=[Path(h5_path).stem for h5_path in h5_paths], + self.buffer_paths = initialize_paths( + filenames=[Path(h5_path).stem for h5_path in h5_paths], folder=folder, subfolder="buffer", prefix=prefix, @@ -685,11 +646,9 @@ def get_files_to_read( self.missing_h5_files = list(compress(h5_paths, files_to_read)) self.save_paths = list(compress(self.buffer_paths, files_to_read)) - self.num_files = len(self.missing_h5_files) - - print(f"Reading files: {self.num_files} new files of {len(h5_paths)} total.") + print(f"Reading files: {len(self.missing_h5_files)} new files of {len(h5_paths)} total.") - def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: + def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: """ Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. @@ -712,32 +671,30 @@ def _create_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) - def create_buffer_files(self, debug: bool) -> None: + def save_buffer_files(self, debug: bool) -> None: """ Creates the buffer files. Args: debug (bool): Flag to enable debug mode, which serializes the creation. """ - # make sure to not create more jobs than cores available - # TODO: This value should be taken from the configuration - n_cores = min(self.num_files, os.cpu_count() - 1) + n_cores = min(len(self.missing_h5_files), self.n_cores) if n_cores > 0: if debug: for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): - self._create_buffer_file(h5_path, parquet_path) + self._save_buffer_file(h5_path, parquet_path) else: Parallel(n_jobs=n_cores, verbose=10)( - delayed(self._create_buffer_file)(h5_path, parquet_path) + delayed(self._save_buffer_file)(h5_path, parquet_path) for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) ) - def fill_dataframes(self) -> None: + def fill_dataframes(self, files: Sequence[Path]) -> tuple[dd.DataFrame, dd.DataFrame]: """ Reads all parquet files into one dataframe using dask and fills NaN values. """ - dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) - metadata = [pq.read_metadata(file) for file in self.buffer_paths] + dataframe = dd.read_parquet(files, calculate_divisions=True) + metadata = [pq.read_metadata(file) for file in files] channels: list[str] = get_channels( self._config["channels"], @@ -747,7 +704,6 @@ def fill_dataframes(self) -> None: index: list[str] = get_channels(index=True) overlap = min(file.num_rows for file in metadata) - print("Filling nan values...") dataframe = forward_fill_lazy( df=dataframe, columns=channels, @@ -774,8 +730,7 @@ def fill_dataframes(self) -> None: dataframe_electron, config=self._config, ) - self.dataframe_electron = dataframe_electron.astype(dtypes) - self.dataframe_pulse = dataframe[index + channels] + return dataframe_electron.astype(dtypes), dataframe[index + channels] def run( self, @@ -803,9 +758,9 @@ def run( if not force_recreate: self.schema_check() - self.create_buffer_files(debug) + self.save_buffer_files(debug) - self.fill_dataframes() + self.dataframe_electron, self.dataframe_pulse = self.fill_dataframes(self.buffer_paths) LOADER = FlashLoader diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 764f64cd..cae76168 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -82,42 +82,44 @@ def get_channels( return channels -def initialize_parquet_paths( - parquet_names: str | list[str] = None, +def initialize_paths( + filenames: str | list[str] = None, folder: Path = None, subfolder: str = "", prefix: str = "", suffix: str = "", extension: str = "parquet", - parquet_paths: list[Path] = None, + paths: list[Path] = None, ) -> list[Path]: """ - Initialize the paths for the Parquet files. + Initialize the paths for files to be saved/loaded. If custom paths are provided, they will be used. Otherwise, paths will be generated based on the specified parameters during initialization. Args: - parquet_paths (List[Path]): Optional custom paths for the Parquet files. + paths (List[Path]): Optional custom paths for the Parquet files. """ - # if parquet_names is string, convert it to a list - if isinstance(parquet_names, str): - parquet_names = [parquet_names] + # if filenames is string, convert it to a list + if isinstance(filenames, str): + filenames = [filenames] # Check if the folder and Parquet paths are provided - if not folder and not parquet_paths: - raise ValueError("Please provide folder or parquet_paths.") - if folder and not parquet_names: - raise ValueError("With folder, please provide parquet_names.") + if not folder and not paths: + raise ValueError("Please provide folder or paths.") + if folder and not filenames: + raise ValueError("With folder, please provide filenames.") # Otherwise create the full path for the Parquet file - parquet_dir = folder.joinpath(subfolder) - parquet_dir.mkdir(parents=True, exist_ok=True) + directory = folder.joinpath(subfolder) + directory.mkdir(parents=True, exist_ok=True) if extension: - extension = f".{extension}" # to be backwards compatible - parquet_paths = [ - parquet_dir.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) for name in parquet_names - ] - - return parquet_paths + extension = f".{extension}" # if extension is provided, it is prepended with a dot + if prefix: + prefix = f"{prefix}_" + if suffix: + suffix = f"_{suffix}" + paths = [directory.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) for name in filenames] + + return paths From dbef804741d411b2fcb43400a53decd7d6d2cde3 Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 22:50:39 +0200 Subject: [PATCH 054/300] add instrument option --- sed/config/flash_example_config.yaml | 2 ++ sed/loader/flash/loader.py | 25 +++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index bbc6940c..dfb070d7 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -9,6 +9,8 @@ core: beamtime_id: 11013410 # the year of the beamtime year: 2023 + # the instrument used + instrument: hextof # hextof, wespe, etc # The paths to the raw and parquet data directories. If these are not # provided, the loader will try to find the data based on year beamtimeID etc diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 266216ab..e2436153 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -55,6 +55,8 @@ def __init__(self, config: dict) -> None: config (dict): Configuration dictionary. """ super().__init__(config=config) + self.instrument = self._config["core"].get("instrument", "hextof") # default is hextof + self.daq = self._config["dataframe"]["daq"] def initialize_dirs(self) -> tuple[list[Path], Path]: """ @@ -83,10 +85,10 @@ def initialize_dirs(self) -> tuple[list[Path], Path]: try: beamtime_id = self._config["core"]["beamtime_id"] year = self._config["core"]["year"] - daq = self._config["dataframe"]["daq"] + except KeyError as exc: raise ValueError( - "The beamtime_id, year and daq are required.", + "The beamtime_id and year are required.", ) from exc beamtime_dir = Path( @@ -104,8 +106,8 @@ def initialize_dirs(self) -> tuple[list[Path], Path]: if dir_name.startswith("express-") or dir_name.startswith( "online-", ): - data_raw_dir.append(path.joinpath(daq)) - elif dir_name == daq.upper(): + data_raw_dir.append(path.joinpath(self.daq)) + elif dir_name == self.daq.upper(): data_raw_dir.append(path) if not data_raw_dir: @@ -143,6 +145,7 @@ def get_files_from_run_id( Raises: FileNotFoundError: If no files are found for the given run in the directory. """ + _ = kwds # not used # Define the stream name prefixes based on the data acquisition identifier stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] @@ -152,10 +155,8 @@ def get_files_from_run_id( if isinstance(folders, str): folders = [folders] - daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) - # Generate the file patterns to search for in the directory - file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension + file_pattern = f"{stream_name_prefixes[self.daq]}_run{run_id}_*." + extension files: list[Path] = [] # Use pathlib to search for matching files in each directory @@ -254,7 +255,6 @@ def read_dataframe( run_id=run, folders=[str(folder.resolve()) for folder in data_raw_dir], extension=ftype, - daq=self._config["dataframe"]["daq"], ) run_files_dict[str(run)] = run_files files.extend(run_files) @@ -292,6 +292,9 @@ def read_dataframe( df = bh.dataframe_electron df_timed = bh.dataframe_pulse + if self.instrument == "wespe": + df, df_timed = wespe_convert(df, df_timed) + metadata = self.parse_metadata(**kwds) if collect_metadata else {} print(f"loading complete in {time.time() - t0: .2f} s") @@ -763,4 +766,10 @@ def run( self.dataframe_electron, self.dataframe_pulse = self.fill_dataframes(self.buffer_paths) +def wespe_convert(df: dd.DataFrame, df_timed: dd.DataFrame) -> tuple[dd.DataFrame, dd.DataFrame]: + df + df_timed + raise NotImplementedError("This function is not implemented yet.") + + LOADER = FlashLoader From afd9772ca1738b7e420a86a6ea34206853557640 Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 23:00:51 +0200 Subject: [PATCH 055/300] fix tests --- tests/loader/flash/test_buffer_handler.py | 30 +++++++++++----------- tests/loader/flash/test_flash_loader.py | 21 ++++++++------- tests/loader/flash/test_parquet_handler.py | 16 ++++++------ 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 4b62c553..d0f31cd2 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -19,10 +19,10 @@ def test_get_files_to_read(config, h5_paths): folder = create_parquet_dir(config, "get_files_to_read") subfolder = folder.joinpath("buffer") # set to false to avoid creating buffer files unnecessarily - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.get_files_to_read(h5_paths, folder, "", "", False) - assert bh.num_files == len(h5_paths) + assert len(bh.missing_h5_files) == len(h5_paths) assert len(bh.save_paths) == len(h5_paths) assert np.all(bh.missing_h5_files == h5_paths) @@ -33,15 +33,15 @@ def test_get_files_to_read(config, h5_paths): assert np.all(bh.save_paths == expected_buffer_paths) # create only one buffer file - bh._create_buffer_file(h5_paths[0], expected_buffer_paths[0]) + bh._save_buffer_file(h5_paths[0], expected_buffer_paths[0]) # check again for files to read bh.get_files_to_read(h5_paths, folder, "", "", False) # check that only one file is to be read - assert bh.num_files == len(h5_paths) - 1 + assert len(bh.missing_h5_files) == len(h5_paths) - 1 Path(expected_buffer_paths[0]).unlink() # remove buffer file # add prefix and suffix - bh.get_files_to_read(h5_paths, folder, "prefix_", "_suffix", False) + bh.get_files_to_read(h5_paths, folder, "prefix", "suffix", False) # expected buffer paths with prefix and suffix expected_buffer_paths = [ @@ -67,7 +67,7 @@ def test_buffer_schema_mismatch(config, h5_paths): - Clean up created buffer files after the test. """ folder = create_parquet_dir(config, "schema_mismatch") - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, debug=True) # Manipulate the configuration to introduce a new channel 'gmdTunnel2' @@ -80,7 +80,7 @@ def test_buffer_schema_mismatch(config, h5_paths): # Reread the dataframe with the modified configuration, expecting a schema mismatch error with pytest.raises(ValueError) as e: - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args @@ -90,7 +90,7 @@ def test_buffer_schema_mismatch(config, h5_paths): assert expected_error[4] == "Please check the configuration file or set force_recreate to True." # Force recreation of the dataframe, including the added channel 'gmdTunnel2' - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, force_recreate=True, debug=True) # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario @@ -98,7 +98,7 @@ def test_buffer_schema_mismatch(config, h5_paths): # also results in error but different from before with pytest.raises(ValueError) as e: # Attempt to read the dataframe again to check for the missing channel error - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args @@ -109,13 +109,13 @@ def test_buffer_schema_mismatch(config, h5_paths): [path.unlink() for path in bh.buffer_paths] -def test_create_buffer_files(config, h5_paths): - folder_serial = create_parquet_dir(config, "create_buffer_files_serial") - bh_serial = BufferHandler(config["dataframe"]) +def test_save_buffer_files(config, h5_paths): + folder_serial = create_parquet_dir(config, "save_buffer_files_serial") + bh_serial = BufferHandler(config) bh_serial.run(h5_paths, folder_serial, debug=True) - folder_parallel = create_parquet_dir(config, "create_buffer_files_parallel") - bh_parallel = BufferHandler(config["dataframe"]) + folder_parallel = create_parquet_dir(config, "save_buffer_files_parallel") + bh_parallel = BufferHandler(config) bh_parallel.run(h5_paths, folder_parallel) df_serial = pd.read_parquet(folder_serial) @@ -131,7 +131,7 @@ def test_create_buffer_files(config, h5_paths): def test_get_filled_dataframe(config, h5_paths): """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") - bh = BufferHandler(config["dataframe"]) + bh = BufferHandler(config) bh.run(h5_paths, folder) df = pd.read_parquet(folder) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index c466316d..bce97e6b 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -29,7 +29,7 @@ def fixture_config_file() -> dict: "sub_dir", ["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], ) -def test_initialize_dir( +def test_initialize_dirs( config_file: dict, fs, sub_dir: Literal["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], @@ -59,9 +59,9 @@ def test_initialize_dir( fs.create_dir(expected_raw_path) fs.create_dir(expected_processed_path) - # Instance of class with correct config and call initialize_dir + # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) - data_raw_dir, data_parquet_dir = fl.initialize_dir() + data_raw_dir, data_parquet_dir = fl.initialize_dirs() assert expected_raw_path == data_raw_dir[0] assert expected_processed_path == data_parquet_dir @@ -70,12 +70,12 @@ def test_initialize_dir( del config["core"]["beamtime_id"] fl = FlashLoader(config=config) with pytest.raises(ValueError) as e: - _, _ = fl.initialize_dir() + _, _ = fl.initialize_dirs() - assert "The beamtime_id, year and daq are required." in str(e.value) + assert "The beamtime_id and year are required." in str(e.value) -def test_initialize_dir_filenotfound(config_file: dict) -> None: +def test_initialize_dirs_filenotfound(config_file: dict) -> None: """ Test FileNotFoundError during the initialization of paths. """ @@ -85,10 +85,10 @@ def test_initialize_dir_filenotfound(config_file: dict) -> None: config["core"]["beamtime_id"] = "11111111" config["core"]["year"] = "2000" - # Instance of class with correct config and call initialize_dir + # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = fl.initialize_dir() + _, _ = fl.initialize_dirs() def test_save_read_parquet_flash(config): @@ -103,4 +103,7 @@ def test_save_read_parquet_flash(config): df2, _, _ = fl.read_dataframe(runs=[43878, 43879], load_parquet=True) # check if parquet read is same as parquet saved read correctly - pd.testing.assert_frame_equal(df1.compute().reset_index(drop=True), df2.compute()) + pd.testing.assert_frame_equal( + df1.compute().reset_index(drop=True), + df2.compute().reset_index(drop=True), + ) diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py index db7d084f..1cdc1c08 100644 --- a/tests/loader/flash/test_parquet_handler.py +++ b/tests/loader/flash/test_parquet_handler.py @@ -2,7 +2,7 @@ import pytest -from sed.loader.flash.utils import initialize_parquet_paths +from sed.loader.flash.utils import initialize_paths def create_parquet_dir(config, folder): @@ -15,30 +15,30 @@ def create_parquet_dir(config, folder): def test_parquet_init_error(): """Test ParquetHandler initialization error""" with pytest.raises(ValueError) as e: - _ = initialize_parquet_paths(parquet_names="test") + _ = initialize_paths(filenames="test") - assert "Please provide folder or parquet_paths." in str(e.value) + assert "Please provide folder or paths." in str(e.value) with pytest.raises(ValueError) as e: - _ = initialize_parquet_paths(folder="test") + _ = initialize_paths(folder="test") - assert "With folder, please provide parquet_names." in str(e.value) + assert "With folder, please provide filenames." in str(e.value) def test_initialize_paths(config): """Test ParquetHandler initialization""" folder = create_parquet_dir(config, "parquet_init") - ph = initialize_parquet_paths("test", folder, extension="xyz") + ph = initialize_paths("test", folder, extension="xyz") assert ph[0].suffix == ".xyz" assert ph[0].name == "test.xyz" # test prefix and suffix - ph = initialize_parquet_paths("test", folder, prefix="prefix_", suffix="_suffix") + ph = initialize_paths("test", folder, prefix="prefix", suffix="suffix") assert ph[0].name == "prefix_test_suffix.parquet" # test with list of parquet_names and subfolder - ph = initialize_parquet_paths(["test1", "test2"], folder, subfolder="subfolder") + ph = initialize_paths(["test1", "test2"], folder, subfolder="subfolder") assert ph[0].parent.name == "subfolder" assert ph[0].name == "test1.parquet" assert ph[1].name == "test2.parquet" From b9fce76ba96f97261395c742f6be46df4c24d1eb Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 23:05:11 +0200 Subject: [PATCH 056/300] fix tests --- sed/loader/sxp/loader.py | 4 ++-- tests/loader/test_loaders.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 67faca4c..3c6b1444 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -53,7 +53,7 @@ def __init__(self, config: dict) -> None: self.failed_files_error: List[str] = [] self.array_indices: List[List[slice]] = None - def initialize_paths(self) -> Tuple[List[Path], Path]: + def initialize_dirs(self) -> Tuple[List[Path], Path]: """ Initializes the paths based on the configuration. @@ -940,7 +940,7 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self.initialize_paths() + data_raw_dir, data_parquet_dir = self.initialize_dirs() # Prepare a list of names for the runs to read and parquets to write if runs is not None: diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index f638ba0d..08ff15b7 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -163,7 +163,7 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -196,7 +196,7 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loaded_timed_dataframe is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -206,7 +206,7 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -240,7 +240,7 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loaded_time is None and loaded_countrate is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -251,7 +251,7 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -285,7 +285,7 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if elapsed_time is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -296,7 +296,7 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_paths() + _, parquet_data_dir = loader.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) From 6400878defdc6e8cc548f02ddd0b73b4be05aa82 Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 23:07:59 +0200 Subject: [PATCH 057/300] fix tests --- tests/loader/sxp/test_sxp_loader.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 3332f231..73d470fd 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -74,7 +74,7 @@ def test_get_channels_by_format(config_file: dict) -> None: ) -def test_initialize_paths(config_file: dict, fs) -> None: +def test_initialize_dirs(config_file: dict, fs) -> None: """ Test the initialization of paths based on the configuration and directory structures. @@ -97,15 +97,15 @@ def test_initialize_paths(config_file: dict, fs) -> None: fs.create_dir(expected_raw_path) fs.create_dir(expected_processed_path) - # Instance of class with correct config and call initialize_paths + # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) - data_raw_dir, data_parquet_dir = sl.initialize_paths() + data_raw_dir, data_parquet_dir = sl.initialize_dirs() assert expected_raw_path == data_raw_dir[0] assert expected_processed_path == data_parquet_dir -def test_initialize_paths_filenotfound(config_file: dict): +def test_initialize_dirs_filenotfound(config_file: dict): """ Test FileNotFoundError during the initialization of paths. """ @@ -115,10 +115,10 @@ def test_initialize_paths_filenotfound(config_file: dict): config["core"]["beamtime_id"] = "11111111" config["core"]["year"] = "2000" - # Instance of class with correct config and call initialize_paths + # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = sl.initialize_paths() + _, _ = sl.initialize_dirs() def test_invalid_channel_format(config_file: dict): @@ -209,6 +209,6 @@ def test_buffer_schema_mismatch(config_file: dict): assert expected_error[3] == "Missing in config: {'delayStage2'}" # Clean up created buffer files after the test - _, parquet_data_dir = sl.initialize_paths() + _, parquet_data_dir = sl.initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) From 7129f57d7cebf5b0b17e2ae2a422cc6286e5e61e Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 19 May 2024 23:19:40 +0200 Subject: [PATCH 058/300] fix tests --- sed/loader/flash/loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index e2436153..c8115c9b 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -56,7 +56,6 @@ def __init__(self, config: dict) -> None: """ super().__init__(config=config) self.instrument = self._config["core"].get("instrument", "hextof") # default is hextof - self.daq = self._config["dataframe"]["daq"] def initialize_dirs(self) -> tuple[list[Path], Path]: """ @@ -106,8 +105,8 @@ def initialize_dirs(self) -> tuple[list[Path], Path]: if dir_name.startswith("express-") or dir_name.startswith( "online-", ): - data_raw_dir.append(path.joinpath(self.daq)) - elif dir_name == self.daq.upper(): + data_raw_dir.append(path.joinpath(self._config["dataframe"]["daq"])) + elif dir_name == self._config["dataframe"]["daq"].upper(): data_raw_dir.append(path) if not data_raw_dir: @@ -145,7 +144,6 @@ def get_files_from_run_id( Raises: FileNotFoundError: If no files are found for the given run in the directory. """ - _ = kwds # not used # Define the stream name prefixes based on the data acquisition identifier stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] @@ -155,8 +153,10 @@ def get_files_from_run_id( if isinstance(folders, str): folders = [folders] + daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) + # Generate the file patterns to search for in the directory - file_pattern = f"{stream_name_prefixes[self.daq]}_run{run_id}_*." + extension + file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension files: list[Path] = [] # Use pathlib to search for matching files in each directory From 02aee6eadd5e9785395cb4e9ba8d40833c6dfae2 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 5 Jun 2024 18:26:29 +0200 Subject: [PATCH 059/300] - added retrocompabtibility for older buffer files that have sectorID saved - tracks if the split_dld_time_from_sector_id is applied or not - metadata tracked for each buffer file - simplified naming --- sed/loader/flash/loader.py | 58 ++++++++++++++++++++------------ sed/loader/sxp/loader.py | 2 +- sed/loader/utils.py | 69 +++++++++++++++++++++++++++----------- 3 files changed, 86 insertions(+), 43 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index c8115c9b..d9d8aa3b 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -33,6 +33,7 @@ from sed.loader.flash.metadata import MetadataRetriever from sed.loader.flash.utils import get_channels from sed.loader.flash.utils import initialize_paths +from sed.loader.utils import get_parquet_metadata from sed.loader.utils import split_dld_time_from_sector_id @@ -247,7 +248,6 @@ def read_dataframe( # Prepare a list of names for the runs to read and parquets to write if runs is not None: files = [] - run_files_dict = {} # Useful for saving entire runs if isinstance(runs, (str, int)): runs = [runs] for run in runs: @@ -256,7 +256,6 @@ def read_dataframe( folders=[str(folder.resolve()) for folder in data_raw_dir], extension=ftype, ) - run_files_dict[str(run)] = run_files files.extend(run_files) self.runs = list(runs) super().read_dataframe(files=files, ftype=ftype) @@ -289,13 +288,15 @@ def read_dataframe( suffix=detector, debug=debug, ) - df = bh.dataframe_electron - df_timed = bh.dataframe_pulse + df = bh.df_electron + df_timed = bh.df_pulse if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) metadata = self.parse_metadata(**kwds) if collect_metadata else {} + metadata.update(bh.metadata) + print(f"loading complete in {time.time() - t0: .2f} s") return df, df_timed, metadata @@ -576,10 +577,11 @@ def __init__( self.missing_h5_files: list[Path] = [] self.save_paths: list[Path] = [] - self.dataframe_electron: dd.DataFrame = None - self.dataframe_pulse: dd.DataFrame = None + self.df_electron: dd.DataFrame = None + self.df_pulse: dd.DataFrame = None + self.metadata: dict = {} - def schema_check(self) -> None: + def _schema_check(self) -> None: """ Checks the schema of the Parquet files. @@ -593,6 +595,10 @@ def schema_check(self) -> None: ) for i, schema in enumerate(parquet_schemas): + if self._config["sector_id_column"] in schema.names: + config_schema.add( + self._config["sector_id_column"], + ) # for compatibility with old files schema_set = set(schema.names) if schema_set != config_schema: missing_in_parquet = config_schema - schema_set @@ -613,7 +619,7 @@ def schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def get_files_to_read( + def _get_files_to_read( self, h5_paths: list[Path], folder: Path, @@ -674,7 +680,7 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) - def save_buffer_files(self, debug: bool) -> None: + def _save_buffer_files(self, debug: bool) -> None: """ Creates the buffer files. @@ -692,12 +698,12 @@ def save_buffer_files(self, debug: bool) -> None: for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) ) - def fill_dataframes(self, files: Sequence[Path]) -> tuple[dd.DataFrame, dd.DataFrame]: + def _fill_dataframes(self): """ Reads all parquet files into one dataframe using dask and fills NaN values. """ - dataframe = dd.read_parquet(files, calculate_divisions=True) - metadata = [pq.read_metadata(file) for file in files] + dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) + file_metadata = get_parquet_metadata(self.buffer_paths) channels: list[str] = get_channels( self._config["channels"], @@ -705,18 +711,23 @@ def fill_dataframes(self, files: Sequence[Path]) -> tuple[dd.DataFrame, dd.DataF extend_aux=True, ) index: list[str] = get_channels(index=True) - overlap = min(file.num_rows for file in metadata) - + overlap = min(file["num_rows"] for file in file_metadata.values()) + self.metadata["buffer_file_metadata"] = file_metadata dataframe = forward_fill_lazy( df=dataframe, columns=channels, before=overlap, iterations=self._config.get("forward_fill_iterations", 2), ) + self.metadata["forward_fill"] = { + "columns": channels, + "overlap": overlap, + "iterations": self._config.get("forward_fill_iterations", 2), + } # Drop rows with nan values in the tof column tof_column = self._config.get("tof_column", "dldTimeSteps") - dataframe_electron = dataframe.dropna(subset=tof_column) + df_electron = dataframe.dropna(subset=tof_column) # Set the dtypes of the channels here as there should be no null values channel_dtypes = get_channels(self._config["channels"], "all") @@ -729,11 +740,14 @@ def fill_dataframes(self, files: Sequence[Path]) -> tuple[dd.DataFrame, dd.DataF # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): - dataframe_electron = split_dld_time_from_sector_id( - dataframe_electron, + df_electron, meta = split_dld_time_from_sector_id( + df_electron, config=self._config, ) - return dataframe_electron.astype(dtypes), dataframe[index + channels] + self.metadata.update(meta) + + self.df_electron = df_electron.astype(dtypes) + self.df_pulse = dataframe[index + channels] def run( self, @@ -756,14 +770,14 @@ def run( debug (bool): Flag to enable debug mode.): """ - self.get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + self._get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) if not force_recreate: - self.schema_check() + self._schema_check() - self.save_buffer_files(debug) + self._save_buffer_files(debug) - self.dataframe_electron, self.dataframe_pulse = self.fill_dataframes(self.buffer_paths) + self._fill_dataframes() def wespe_convert(df: dd.DataFrame, df_timed: dd.DataFrame) -> tuple[dd.DataFrame, dd.DataFrame]: diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 3c6b1444..aa018b91 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -655,7 +655,7 @@ def create_dataframe_per_file( df = df.dropna(subset=self._config["dataframe"].get("tof_column", "dldTimeSteps")) # correct the 3 bit shift which encodes the detector ID in the 8s time if self._config["dataframe"].get("split_sector_id_from_dld_time", False): - df = split_dld_time_from_sector_id(df, config=self._config) + df, _ = split_dld_time_from_sector_id(df, config=self._config) return df def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, Exception]: diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 0d26671c..3532b4c9 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -1,14 +1,16 @@ """Utilities for loaders """ +from __future__ import annotations + from glob import glob +from pathlib import Path from typing import cast -from typing import List from typing import Sequence -from typing import Union import dask.dataframe import numpy as np import pandas as pd +import pyarrow.parquet as pq from h5py import File from h5py import Group from natsort import natsorted @@ -21,7 +23,7 @@ def gather_files( f_end: int = None, f_step: int = 1, file_sorting: bool = True, -) -> List[str]: +) -> list[str]: """Collects and sorts files with specified extension from a given folder. Args: @@ -37,13 +39,13 @@ def gather_files( Defaults to True. Returns: - List[str]: List of collected file names. + list[str]: list of collected file names. """ try: files = glob(folder + "/*." + extension) if file_sorting: - files = cast(List[str], natsorted(files)) + files = cast(list[str], natsorted(files)) if f_start is not None and f_end is not None: files = files[slice(f_start, f_end, f_step)] @@ -55,7 +57,7 @@ def gather_files( return files -def parse_h5_keys(h5_file: File, prefix: str = "") -> List[str]: +def parse_h5_keys(h5_file: File, prefix: str = "") -> list[str]: """Helper method which parses the channels present in the h5 file Args: h5_file (h5py.File): The H5 file object. @@ -63,7 +65,7 @@ def parse_h5_keys(h5_file: File, prefix: str = "") -> List[str]: Defaults to an empty string. Returns: - List[str]: A list of channel names in the H5 file. + list[str]: A list of channel names in the H5 file. Raises: Exception: If an error occurs while parsing the keys. @@ -144,12 +146,12 @@ def split_channel_bitwise( def split_dld_time_from_sector_id( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, tof_column: str = None, sector_id_column: str = None, sector_id_reserved_bits: int = None, config: dict = None, -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Converts the 8s time in steps to time in steps and sectorID. The 8s detector encodes the dldSectorID in the 3 least significant bits of the @@ -183,15 +185,42 @@ def split_dld_time_from_sector_id( raise ValueError('No value for "sector_id_reserved_bits" found in config.') if sector_id_column in df.columns: - raise ValueError( - f"Column {sector_id_column} already in dataframe. This function is not idempotent.", + metadata = {"applied": False, "reason": f"Column {sector_id_column} already in dataframe"} + else: + # Split the time-of-flight column into sector ID and time-of-flight steps + df = split_channel_bitwise( + df=df, + input_column=tof_column, + output_columns=[sector_id_column, tof_column], + bit_mask=sector_id_reserved_bits, + overwrite=True, + types=[np.int8, np.int32], ) - df = split_channel_bitwise( - df=df, - input_column=tof_column, - output_columns=[sector_id_column, tof_column], - bit_mask=sector_id_reserved_bits, - overwrite=True, - types=[np.int8, np.int32], - ) - return df + metadata = { + "applied": True, + "tof_column": tof_column, + "sector_id_column": sector_id_column, + "sector_id_reserved_bits": sector_id_reserved_bits, + } + + return df, {"split_dld_time_from_sector_id": metadata} + + +def get_parquet_metadata(file_paths: list[Path]) -> dict[str, dict]: + organized_metadata = {} + for i, file_path in enumerate(file_paths): + # Read the metadata for the file + file_meta = pq.read_metadata(file_path) + + # Convert the metadata to a dictionary and remove "row_groups" + # as they contain a lot of info that is not needed + metadata_dict = file_meta.to_dict() + metadata_dict.pop("row_groups", None) + + # Add the filename to the metadata dictionary + metadata_dict["filename"] = str(file_path.name) + + # Add the metadata dictionary to the organized_metadata dictionary + organized_metadata[f"file_{i + 1}"] = metadata_dict + + return organized_metadata From 2142c1118d39aec0d67e3d10d70ade578522f722 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 5 Jun 2024 21:11:47 +0200 Subject: [PATCH 060/300] fix ruff settings --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cef05707..7f535dab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,10 +15,10 @@ repos: # Ruff version. rev: v0.2.2 hooks: - # Run the linter. - - id: ruff # Run the formatter. - id: ruff-format + # Run the linter. + - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.7.1 hooks: From 79922efe770f4a354ae77d55c2a1b7fa9c4019b5 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 5 Jun 2024 21:40:14 +0200 Subject: [PATCH 061/300] update tests --- tests/loader/flash/test_buffer_handler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index d0f31cd2..be59c59b 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -20,7 +20,7 @@ def test_get_files_to_read(config, h5_paths): subfolder = folder.joinpath("buffer") # set to false to avoid creating buffer files unnecessarily bh = BufferHandler(config) - bh.get_files_to_read(h5_paths, folder, "", "", False) + bh._get_files_to_read(h5_paths, folder, "", "", False) assert len(bh.missing_h5_files) == len(h5_paths) assert len(bh.save_paths) == len(h5_paths) @@ -35,13 +35,13 @@ def test_get_files_to_read(config, h5_paths): # create only one buffer file bh._save_buffer_file(h5_paths[0], expected_buffer_paths[0]) # check again for files to read - bh.get_files_to_read(h5_paths, folder, "", "", False) + bh._get_files_to_read(h5_paths, folder, "", "", False) # check that only one file is to be read assert len(bh.missing_h5_files) == len(h5_paths) - 1 Path(expected_buffer_paths[0]).unlink() # remove buffer file # add prefix and suffix - bh.get_files_to_read(h5_paths, folder, "prefix", "suffix", False) + bh._get_files_to_read(h5_paths, folder, "prefix", "suffix", False) # expected buffer paths with prefix and suffix expected_buffer_paths = [ @@ -136,7 +136,7 @@ def test_get_filled_dataframe(config, h5_paths): df = pd.read_parquet(folder) - assert np.all(list(bh.dataframe_electron.columns) == list(df.columns) + ["dldSectorID"]) + assert np.all(list(bh.df_electron.columns) == list(df.columns) + ["dldSectorID"]) channel_pulse = get_channels( config["dataframe"]["channels"], @@ -144,6 +144,6 @@ def test_get_filled_dataframe(config, h5_paths): index=True, extend_aux=True, ) - assert np.all(list(bh.dataframe_pulse.columns) == channel_pulse) + assert np.all(list(bh.df_pulse.columns) == channel_pulse) # remove buffer files [path.unlink() for path in bh.buffer_paths] From 02ae74ed457fef235fd36ebe0e07958a4058caf7 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 5 Jun 2024 22:37:54 +0200 Subject: [PATCH 062/300] make small change to check actions status --- sed/loader/flash/loader.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index d9d8aa3b..1d2411b9 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -56,7 +56,8 @@ def __init__(self, config: dict) -> None: config (dict): Configuration dictionary. """ super().__init__(config=config) - self.instrument = self._config["core"].get("instrument", "hextof") # default is hextof + self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof + self._metadata: dict = {} def initialize_dirs(self) -> tuple[list[Path], Path]: """ @@ -244,7 +245,7 @@ def read_dataframe( t0 = time.time() data_raw_dir, data_parquet_dir = self.initialize_dirs() - + self._metadata.update(metadata) # Prepare a list of names for the runs to read and parquets to write if runs is not None: files = [] @@ -294,12 +295,12 @@ def read_dataframe( if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) - metadata = self.parse_metadata(**kwds) if collect_metadata else {} - metadata.update(bh.metadata) + self._metadata.update(self.parse_metadata(**kwds) if collect_metadata else {}) + self._metadata.update(bh.metadata) print(f"loading complete in {time.time() - t0: .2f} s") - return df, df_timed, metadata + return df, df_timed, self._metadata class DataFrameCreator: From f520310e790df79c517a9850c9db155d264d795f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 6 Jun 2024 00:08:58 +0200 Subject: [PATCH 063/300] bring back types --- sed/loader/utils.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 3532b4c9..f3656866 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -1,11 +1,13 @@ """Utilities for loaders """ -from __future__ import annotations - from glob import glob from pathlib import Path from typing import cast +from typing import Dict +from typing import List from typing import Sequence +from typing import Tuple +from typing import Union import dask.dataframe import numpy as np @@ -23,7 +25,7 @@ def gather_files( f_end: int = None, f_step: int = 1, file_sorting: bool = True, -) -> list[str]: +) -> List[str]: """Collects and sorts files with specified extension from a given folder. Args: @@ -39,13 +41,13 @@ def gather_files( Defaults to True. Returns: - list[str]: list of collected file names. + List[str]: List of collected file names. """ try: files = glob(folder + "/*." + extension) if file_sorting: - files = cast(list[str], natsorted(files)) + files = cast(List[str], natsorted(files)) if f_start is not None and f_end is not None: files = files[slice(f_start, f_end, f_step)] @@ -57,7 +59,7 @@ def gather_files( return files -def parse_h5_keys(h5_file: File, prefix: str = "") -> list[str]: +def parse_h5_keys(h5_file: File, prefix: str = "") -> List[str]: """Helper method which parses the channels present in the h5 file Args: h5_file (h5py.File): The H5 file object. @@ -65,7 +67,7 @@ def parse_h5_keys(h5_file: File, prefix: str = "") -> list[str]: Defaults to an empty string. Returns: - list[str]: A list of channel names in the H5 file. + List[str]: A list of channel names in the H5 file. Raises: Exception: If an error occurs while parsing the keys. @@ -146,12 +148,12 @@ def split_channel_bitwise( def split_dld_time_from_sector_id( - df: pd.DataFrame | dask.dataframe.DataFrame, + df: Union[pd.DataFrame, dask.dataframe.DataFrame], tof_column: str = None, sector_id_column: str = None, sector_id_reserved_bits: int = None, config: dict = None, -) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: +) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], Dict]: """Converts the 8s time in steps to time in steps and sectorID. The 8s detector encodes the dldSectorID in the 3 least significant bits of the @@ -206,7 +208,7 @@ def split_dld_time_from_sector_id( return df, {"split_dld_time_from_sector_id": metadata} -def get_parquet_metadata(file_paths: list[Path]) -> dict[str, dict]: +def get_parquet_metadata(file_paths: List[Path]) -> Dict[str, Dict]: organized_metadata = {} for i, file_path in enumerate(file_paths): # Read the metadata for the file From 4c7d06905638e79baad7e425e9be6829045aa1c0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 6 Jun 2024 00:20:45 +0200 Subject: [PATCH 064/300] fix small error --- sed/loader/flash/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 1d2411b9..f19a1cfb 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -211,7 +211,7 @@ def read_dataframe( folders: str | Sequence[str] = None, runs: str | Sequence[str] = None, ftype: str = "h5", - metadata: dict = None, + metadata: dict = {}, collect_metadata: bool = False, detector: str = "", force_recreate: bool = False, From 9f6a31b368026e05d62f30b3b2c94f2a943442a2 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 6 Jun 2024 13:11:14 +0200 Subject: [PATCH 065/300] move utility func test to utility tests --- tests/loader/flash/test_parquet_handler.py | 44 ---------------------- tests/loader/flash/test_utils.py | 44 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 tests/loader/flash/test_parquet_handler.py diff --git a/tests/loader/flash/test_parquet_handler.py b/tests/loader/flash/test_parquet_handler.py deleted file mode 100644 index 1cdc1c08..00000000 --- a/tests/loader/flash/test_parquet_handler.py +++ /dev/null @@ -1,44 +0,0 @@ -from pathlib import Path - -import pytest - -from sed.loader.flash.utils import initialize_paths - - -def create_parquet_dir(config, folder): - parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) - parquet_path = parquet_path.joinpath(folder) - parquet_path.mkdir(parents=True, exist_ok=True) - return parquet_path - - -def test_parquet_init_error(): - """Test ParquetHandler initialization error""" - with pytest.raises(ValueError) as e: - _ = initialize_paths(filenames="test") - - assert "Please provide folder or paths." in str(e.value) - - with pytest.raises(ValueError) as e: - _ = initialize_paths(folder="test") - - assert "With folder, please provide filenames." in str(e.value) - - -def test_initialize_paths(config): - """Test ParquetHandler initialization""" - folder = create_parquet_dir(config, "parquet_init") - - ph = initialize_paths("test", folder, extension="xyz") - assert ph[0].suffix == ".xyz" - assert ph[0].name == "test.xyz" - - # test prefix and suffix - ph = initialize_paths("test", folder, prefix="prefix", suffix="suffix") - assert ph[0].name == "prefix_test_suffix.parquet" - - # test with list of parquet_names and subfolder - ph = initialize_paths(["test1", "test2"], folder, subfolder="subfolder") - assert ph[0].parent.name == "subfolder" - assert ph[0].name == "test1.parquet" - assert ph[1].name == "test2.parquet" diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index efd70e89..3643feaf 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -1,5 +1,10 @@ """Tests for utils functionality""" +from pathlib import Path + +import pytest + from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import initialize_paths # Define expected channels for each format. ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] @@ -72,3 +77,42 @@ def test_get_channels_by_format(config_dataframe): ) == set( format_all_index_extend_aux, ) + + +def create_parquet_dir(config, folder): + parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) + parquet_path = parquet_path.joinpath(folder) + parquet_path.mkdir(parents=True, exist_ok=True) + return parquet_path + + +def test_parquet_init_error(): + """Test ParquetHandler initialization error""" + with pytest.raises(ValueError) as e: + _ = initialize_paths(filenames="test") + + assert "Please provide folder or paths." in str(e.value) + + with pytest.raises(ValueError) as e: + _ = initialize_paths(folder="test") + + assert "With folder, please provide filenames." in str(e.value) + + +def test_initialize_paths(config): + """Test ParquetHandler initialization""" + folder = create_parquet_dir(config, "parquet_init") + + ph = initialize_paths("test", folder, extension="xyz") + assert ph[0].suffix == ".xyz" + assert ph[0].name == "test.xyz" + + # test prefix and suffix + ph = initialize_paths("test", folder, prefix="prefix", suffix="suffix") + assert ph[0].name == "prefix_test_suffix.parquet" + + # test with list of parquet_names and subfolder + ph = initialize_paths(["test1", "test2"], folder, subfolder="subfolder") + assert ph[0].parent.name == "subfolder" + assert ph[0].name == "test1.parquet" + assert ph[1].name == "test2.parquet" From f4a30e0a03df8b722d2b6cf4e8f0f13b35669708 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 17:41:41 +0200 Subject: [PATCH 066/300] seperate to different modules --- sed/loader/flash/buffer_handler.py | 244 ++++++++++++ sed/loader/flash/dataframe.py | 264 +++++++++++++ sed/loader/flash/instruments.py | 9 + sed/loader/flash/loader.py | 573 +++-------------------------- 4 files changed, 560 insertions(+), 530 deletions(-) create mode 100644 sed/loader/flash/buffer_handler.py create mode 100644 sed/loader/flash/dataframe.py create mode 100644 sed/loader/flash/instruments.py diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py new file mode 100644 index 00000000..99f2a2e7 --- /dev/null +++ b/sed/loader/flash/buffer_handler.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import os +from itertools import compress +from pathlib import Path + +import dask.dataframe as dd +import h5py +import pyarrow.parquet as pq +from joblib import delayed +from joblib import Parallel + +from sed.core.dfops import forward_fill_lazy +from sed.loader.flash.dataframe import DataFrameCreator +from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import initialize_paths +from sed.loader.utils import get_parquet_metadata +from sed.loader.utils import split_dld_time_from_sector_id + + +class BufferHandler: + """ + A class for handling the creation and manipulation of buffer files using DataFrameCreator. + """ + + def __init__( + self, + config: dict, + ) -> None: + """ + Initializes the BufferHandler. + + Args: + config (dict): The configuration dictionary. + """ + self._config = config["dataframe"] + self.n_cores = config["core"].get("num_cores", os.cpu_count() - 1) + + self.buffer_paths: list[Path] = [] + self.missing_h5_files: list[Path] = [] + self.save_paths: list[Path] = [] + + self.df_electron: dd.DataFrame = None + self.df_pulse: dd.DataFrame = None + self.metadata: dict = {} + + def _schema_check(self) -> None: + """ + Checks the schema of the Parquet files. + + Raises: + ValueError: If the schema of the Parquet files does not match the configuration. + """ + existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] + parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] + config_schema_set = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + + for filename, schema in zip(existing_parquet_filenames, parquet_schemas): + # for retro compatibility when sectorID was also saved in buffer + if self._config["sector_id_column"] in schema.names: + config_schema_set.add( + self._config["sector_id_column"], + ) + schema_set = set(schema.names) + if schema_set != config_schema_set: + missing_in_parquet = config_schema_set - schema_set + missing_in_config = schema_set - config_schema_set + + errors = [] + if missing_in_parquet: + errors.append(f"Missing in parquet: {missing_in_parquet}") + if missing_in_config: + errors.append(f"Missing in config: {missing_in_config}") + + raise ValueError( + f"The available channels do not match the schema of file {filename}. " + f"{' '.join(errors)}", + ) + + def _get_files_to_read( + self, + h5_paths: list[Path], + folder: Path, + prefix: str, + suffix: str, + force_recreate: bool, + ) -> None: + """ + Determines the list of files to read and the corresponding buffer files to create. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + force_recreate (bool): Flag to force recreation of buffer files. + """ + # Getting the paths of the buffer files, with subfolder as buffer and no extension + self.buffer_paths = initialize_paths( + filenames=[h5_path.stem for h5_path in h5_paths], + folder=folder, + subfolder="buffer", + prefix=prefix, + suffix=suffix, + extension="", + ) + # read only the files that do not exist or if force_recreate is True + files_to_read = [ + force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths + ] + + # Get the list of H5 files to read and the corresponding buffer files to create + self.missing_h5_files = list(compress(h5_paths, files_to_read)) + self.save_paths = list(compress(self.buffer_paths, files_to_read)) + + print(f"Reading files: {len(self.missing_h5_files)} new files of {len(h5_paths)} total.") + + def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: + """ + Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. + + Args: + h5_path (Path): Path to the H5 file. + parquet_path (Path): Path to the buffer file. + """ + # Open the h5 file in read mode + h5_file = h5py.File(h5_path, "r") + + # Create a DataFrameCreator instance with the configuration and the h5 file + dfc = DataFrameCreator(config_dataframe=self._config, h5_file=h5_file) + + # Get the DataFrame from the DataFrameCreator instance + df = dfc.df + + # Close the h5 file + h5_file.close() + + # Reset the index of the DataFrame and save it as a parquet file + df.reset_index().to_parquet(parquet_path) + + def _save_buffer_files(self, debug: bool) -> None: + """ + Creates the buffer files. + + Args: + debug (bool): Flag to enable debug mode, which serializes the creation. + """ + n_cores = min(len(self.missing_h5_files), self.n_cores) + if n_cores > 0: + if debug: + for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): + self._save_buffer_file(h5_path, parquet_path) + else: + Parallel(n_jobs=n_cores, verbose=10)( + delayed(self._save_buffer_file)(h5_path, parquet_path) + for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) + ) + + def _fill_dataframes(self): + """ + Reads all parquet files into one dataframe using dask and fills NaN values. + """ + dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) + file_metadata = get_parquet_metadata( + self.buffer_paths, + time_stamp_col=self._config["time_stamp_alias", "timeStamp"], + ) + self.metadata["file_statistics"] = file_metadata + + channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + index: list[str] = get_channels(index=True) + overlap = min(file["num_rows"] for file in file_metadata.values()) + + dataframe = forward_fill_lazy( + df=dataframe, + columns=channels, + before=overlap, + iterations=self._config.get("forward_fill_iterations", 2), + ) + self.metadata["forward_fill"] = { + "columns": channels, + "overlap": overlap, + "iterations": self._config.get("forward_fill_iterations", 2), + } + + # Drop rows with nan values in the tof column + tof_column = self._config.get("tof_column", "dldTimeSteps") + df_electron = dataframe.dropna(subset=tof_column) + + # Set the dtypes of the channels here as there should be no null values + channel_dtypes = get_channels(self._config["channels"], "all") + config_channels = self._config["channels"] + dtypes = { + channel: config_channels[channel].get("dtype") + for channel in channel_dtypes + if config_channels[channel].get("dtype") is not None + } + + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + df_electron, meta = split_dld_time_from_sector_id( + df_electron, + config=self._config, + ) + self.metadata.update(meta) + + self.df_electron = df_electron.astype(dtypes) + self.df_pulse = dataframe[index + channels] + + def run( + self, + h5_paths: list[Path], + folder: Path, + force_recreate: bool = False, + prefix: str = "", + suffix: str = "", + debug: bool = False, + ) -> None: + """ + Runs the buffer file creation process. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + force_recreate (bool): Flag to force recreation of buffer files. + prefix (str): Prefix for buffer file names. + suffix (str): Suffix for buffer file names. + debug (bool): Flag to enable debug mode.): + """ + + self._get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + + if not force_recreate: + self._schema_check() + + self._save_buffer_files(debug) + + self._fill_dataframes() diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py new file mode 100644 index 00000000..31d71e47 --- /dev/null +++ b/sed/loader/flash/dataframe.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import h5py +import numpy as np +from pandas import concat +from pandas import DataFrame +from pandas import Index +from pandas import MultiIndex +from pandas import Series + +from sed.loader.flash.utils import get_channels + + +class DataFrameCreator: + """ + Utility class for creating pandas DataFrames from HDF5 files with multiple channels. + """ + + def __init__(self, config_dataframe: dict, h5_file: h5py.File) -> None: + """ + Initializes the DataFrameCreator class. + + Args: + config_dataframe (dict): The configuration dictionary with only the dataframe key. + h5_file (h5py.File): The open h5 file. + """ + self.h5_file: h5py.File = h5_file + self.failed_files_error: list[str] = [] + self.multi_index = get_channels(index=True) + self._config = config_dataframe + + def get_index_dataset_key(self, channel: str) -> tuple[str, str]: + """ + Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. + + Args: + channel (str): The name of the channel. + + Returns: + tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. + + Raises: + ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + """ + channel_config = self._config["channels"][channel] + + if "group_name" in channel_config: + index_key = channel_config["group_name"] + "index" + if channel == "timeStamp": + dataset_key = channel_config["group_name"] + "time" + else: + dataset_key = channel_config["group_name"] + "value" + return index_key, dataset_key + if "index_key" in channel_config and "dataset_key" in channel_config: + return channel_config["index_key"], channel_config["dataset_key"] + + raise ValueError( + "For channel:", + channel, + "Provide either both 'index_key' and 'dataset_key'.", + "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", + ) + + def get_dataset_array( + self, + channel: str, + slice_: bool = False, + ) -> tuple[Index, h5py.Dataset]: + """ + Returns a numpy array for a given channel name. + + Args: + channel (str): The name of the channel. + slice_ (bool): If True, applies slicing on the dataset. + + Returns: + tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array + for the channel's data. + """ + # Get the data from the necessary h5 file and channel + index_key, dataset_key = self.get_index_dataset_key(channel) + + key = Index(self.h5_file[index_key], name="trainId") # macrobunch + dataset = self.h5_file[dataset_key] + + if slice_: + slice_index = self._config["channels"][channel].get("slice", None) + if slice_index is not None: + dataset = np.take(dataset, slice_index, axis=1) + # If np_array is size zero, fill with NaNs + if dataset.shape[0] == 0: + # Fill the np_array with NaN values of the same shape as train_id + dataset = np.full_like(key, np.nan, dtype=np.double) + + return key, dataset + + def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: + """ + Computes the index for the 'per_electron' data. + + Args: + offset (int): The offset value. + + Returns: + tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and + the indexer. + """ + # Get the pulseId and the index_train + index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) + # Repeat the index_train by the number of pulses + index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) + # Explode the pulse dataset and subtract by the ubid_offset + pulse_ravel = dataset_pulse.ravel() - offset + # Create a MultiIndex with the index_train and the pulse + microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + + # Only sort if necessary + indexer = slice(None) + if not microbunches.is_monotonic_increasing: + microbunches, indexer = microbunches.sort_values(return_indexer=True) + + # Count the number of electrons per microbunch and create an array of electrons + electron_counts = microbunches.value_counts(sort=False).values + electrons = np.concatenate([np.arange(count) for count in electron_counts]) + + # Final index constructed here + index = MultiIndex.from_arrays( + ( + microbunches.get_level_values(0), + microbunches.get_level_values(1).astype(int), + electrons, + ), + names=self.multi_index, + ) + return index, indexer + + @property + def df_electron(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per electron]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_electron' channel's data. + """ + offset = self._config["ubid_offset"] + # Index + index, indexer = self.pulse_index(offset) + + # Data logic + channels = get_channels(self._config["channels"], "per_electron") + slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + + # First checking if dataset keys are the same for all channels + dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] + all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) + + # If all dataset keys are the same, we can directly use the ndarray to create frame + if all_keys_same: + _, dataset = self.get_dataset_array(channels[0]) + data_dict = { + channel: dataset[:, slice_, :].ravel() + for channel, slice_ in zip(channels, slice_index) + } + dataframe = DataFrame(data_dict) + # Otherwise, we need to create a Series for each channel and concatenate them + else: + series = { + channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + for channel in channels + } + dataframe = concat(series, axis=1) + + drop_vals = np.arange(-offset, 0) + + # Few things happen here: + # Drop all NaN values like while creating the multiindex + # if necessary, the data is sorted with [indexer] + # MultiIndex is set + # Finally, the offset values are dropped + return ( + dataframe.dropna() + .iloc[indexer] + .set_index(index) + .drop(index=drop_vals, level="pulseId", errors="ignore") + ) + + @property + def df_pulse(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per pulse]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. + """ + series = [] + channels = get_channels(self._config["channels"], "per_pulse") + for channel in channels: + # get slice + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, np.arange(0, dataset.shape[1]), [0]), + names=self.multi_index, + ) + series.append(Series(dataset[()].ravel(), index=index, name=channel)) + + return concat(series, axis=1) # much faster when concatenating similarly indexed data first + + @property + def df_train(self) -> DataFrame: + """ + Returns a pandas DataFrame for a given channel name of type [per train]. + + Returns: + DataFrame: The pandas DataFrame for the 'per_train' channel's data. + """ + series = [] + + channels = get_channels(self._config["channels"], "per_train") + + for channel in channels: + key, dataset = self.get_dataset_array(channel, slice_=True) + index = MultiIndex.from_product( + (key, [0], [0]), + names=self.multi_index, + ) + if channel == "dldAux": + aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() + for name, slice_aux in aux_channels: + series.append(Series(dataset[: key.size, slice_aux], index, name=name)) + else: + series.append(Series(dataset, index, name=channel)) + + return concat(series, axis=1) + + def validate_channel_keys(self) -> None: + """ + Validates if the index and dataset keys for all channels in config exist in the h5 file. + + Raises: + KeyError: If the index or dataset keys do not exist in the file. + """ + for channel in self._config["channels"]: + index_key, dataset_key = self.get_index_dataset_key(channel) + if index_key not in self.h5_file: + raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") + if dataset_key not in self.h5_file: + raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") + + @property + def df(self) -> DataFrame: + """ + Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, + returning a single dataframe. + + Returns: + DataFrame: The combined pandas DataFrame. + """ + + self.validate_channel_keys() + return ( + self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") + .join(self.df_train, on=self.multi_index, how="outer") + .sort_index() + ) diff --git a/sed/loader/flash/instruments.py b/sed/loader/flash/instruments.py new file mode 100644 index 00000000..8ef0146e --- /dev/null +++ b/sed/loader/flash/instruments.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from dask import dataframe as dd + + +def wespe_convert(df: dd.DataFrame, df_timed: dd.DataFrame) -> tuple[dd.DataFrame, dd.DataFrame]: + df + df_timed + raise NotImplementedError("This function is not implemented yet.") diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index f19a1cfb..b1939b02 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -9,32 +9,17 @@ """ from __future__ import annotations -import os import time -from itertools import compress from pathlib import Path from typing import Sequence import dask.dataframe as dd -import h5py -import numpy as np -import pyarrow.parquet as pq -from joblib import delayed -from joblib import Parallel from natsort import natsorted -from pandas import concat -from pandas import DataFrame -from pandas import Index -from pandas import MultiIndex -from pandas import Series -from sed.core.dfops import forward_fill_lazy from sed.loader.base.loader import BaseLoader +from sed.loader.flash.buffer_handler import BufferHandler +from sed.loader.flash.instruments import wespe_convert from sed.loader.flash.metadata import MetadataRetriever -from sed.loader.flash.utils import get_channels -from sed.loader.flash.utils import initialize_paths -from sed.loader.utils import get_parquet_metadata -from sed.loader.utils import split_dld_time_from_sector_id class FlashLoader(BaseLoader): @@ -58,16 +43,19 @@ def __init__(self, config: dict) -> None: super().__init__(config=config) self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof self._metadata: dict = {} + self.raw_dir, self.parquet_dir = self._initialize_dirs() - def initialize_dirs(self) -> tuple[list[Path], Path]: + def _initialize_dirs(self) -> tuple[str, str]: """ Initializes the directories on Maxwell based on configuration. If paths is provided in - the configuration, the raw data directories and parquet data directory are taken from there. + the configuration, the raw data directory and parquet data directory are taken from there. Otherwise, the beamtime_id and year are used to locate the data directories. + The first path that has either online- or express- prefix, or the daq name is taken as the + raw data directory. Returns: - Tuple[List[Path], Path]: A tuple containing a list of raw data directories - paths and the parquet data directory path. + Tuple[str, str]: A tuple containing raw data directory + and the parquet data directory. Raises: ValueError: If required values are missing from the configuration. @@ -104,9 +92,7 @@ def initialize_dirs(self) -> tuple[list[Path], Path]: for path in raw_path.glob("**/*"): if path.is_dir(): dir_name = path.name - if dir_name.startswith("express-") or dir_name.startswith( - "online-", - ): + if dir_name.startswith(("online-", "express-")): data_raw_dir.append(path.joinpath(self._config["dataframe"]["daq"])) elif dir_name == self._config["dataframe"]["daq"].upper(): data_raw_dir.append(path) @@ -119,14 +105,13 @@ def initialize_dirs(self) -> tuple[list[Path], Path]: data_parquet_dir.mkdir(parents=True, exist_ok=True) - return data_raw_dir, data_parquet_dir + return str(data_raw_dir[0].resolve()), str(data_parquet_dir) - def get_files_from_run_id( + def get_files_from_run_id( # type: ignore[override] self, run_id: str, folders: str | Sequence[str] = None, extension: str = "h5", - **kwds, ) -> list[str]: """ Returns a list of filenames for a given run located in the specified directory @@ -137,8 +122,6 @@ def get_files_from_run_id( folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"]. extension (str, optional): The file extension. Defaults to "h5". - kwds: Keyword arguments: - - daq (str): The data acquisition identifier. Returns: List[str]: A list of path strings representing the collected file names. @@ -155,7 +138,7 @@ def get_files_from_run_id( if isinstance(folders, str): folders = [folders] - daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) + daq = self._config["dataframe"].get("daq") # Generate the file patterns to search for in the directory file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension @@ -179,6 +162,31 @@ def get_files_from_run_id( # Return the list of found files return [str(file.resolve()) for file in files] + def get_files_from_run_ids( + self, + runs: str | int | Sequence[str | int], + ) -> tuple[list[str], list[str]]: + """ + Retrieves the file paths associated with a given set of run IDs. + + Args: + runs (str | int | Sequence[str | int]): A single run ID or a list of run IDs. + + Returns: + Tuple[List[str], List[str]]: List of runs and List of file paths. + """ + files = [] + if isinstance(runs, (str, int)): + runs = [runs] + runs_ = list(map(str, runs)) # convert all elements in runs to string + for run in runs_: + run_files = self.get_files_from_run_id( + run_id=run, + folders=self.raw_dir, + ) + files.extend(run_files) + return runs_, files + def parse_metadata(self, scicat_token: str = None) -> dict: """Uses the MetadataRetriever class to fetch metadata from scicat for each run. @@ -209,7 +217,7 @@ def read_dataframe( self, files: str | Sequence[str] = None, folders: str | Sequence[str] = None, - runs: str | Sequence[str] = None, + runs: str | int | Sequence[str] | Sequence[int] = None, ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, @@ -244,23 +252,12 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self.initialize_dirs() self._metadata.update(metadata) # Prepare a list of names for the runs to read and parquets to write if runs is not None: - files = [] - if isinstance(runs, (str, int)): - runs = [runs] - for run in runs: - run_files = self.get_files_from_run_id( - run_id=run, - folders=[str(folder.resolve()) for folder in data_raw_dir], - extension=ftype, - ) - files.extend(run_files) - self.runs = list(runs) + runs, files = self.get_files_from_run_ids(runs) + self.runs = runs super().read_dataframe(files=files, ftype=ftype) - else: # This call takes care of files and folders. As we have converted runs into files # already, they are just stored in the class by this call. @@ -275,8 +272,8 @@ def read_dataframe( config=self._config, ) - # if parquet_dir is None, use data_parquet_dir - parquet_dir = parquet_dir or data_parquet_dir + # if parquet_dir is None, use self.parquet_dir + parquet_dir = parquet_dir or self.parquet_dir parquet_dir = Path(parquet_dir) # Obtain the parquet filenames, metadata, and schema from the method @@ -303,488 +300,4 @@ def read_dataframe( return df, df_timed, self._metadata -class DataFrameCreator: - """ - Utility class for creating pandas DataFrames from HDF5 files with multiple channels. - """ - - def __init__(self, config_dataframe: dict, h5_file: h5py.File) -> None: - """ - Initializes the DataFrameCreator class. - - Args: - config_dataframe (dict): The configuration dictionary with only the dataframe key. - h5_file (h5py.File): The open h5 file. - """ - self.h5_file: h5py.File = h5_file - self.failed_files_error: list[str] = [] - self.multi_index = get_channels(index=True) - self._config = config_dataframe - - def get_index_dataset_key(self, channel: str) -> tuple[str, str]: - """ - Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. - - Args: - channel (str): The name of the channel. - - Returns: - tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. - - Raises: - ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. - """ - channel_config = self._config["channels"][channel] - - if "group_name" in channel_config: - index_key = channel_config["group_name"] + "index" - if channel == "timeStamp": - dataset_key = channel_config["group_name"] + "time" - else: - dataset_key = channel_config["group_name"] + "value" - return index_key, dataset_key - if "index_key" in channel_config and "dataset_key" in channel_config: - return channel_config["index_key"], channel_config["dataset_key"] - - raise ValueError( - "For channel:", - channel, - "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", - ) - - def get_dataset_array( - self, - channel: str, - slice_: bool = False, - ) -> tuple[Index, h5py.Dataset]: - """ - Returns a numpy array for a given channel name. - - Args: - channel (str): The name of the channel. - slice_ (bool): If True, applies slicing on the dataset. - - Returns: - tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array - for the channel's data. - """ - # Get the data from the necessary h5 file and channel - index_key, dataset_key = self.get_index_dataset_key(channel) - - key = Index(self.h5_file[index_key], name="trainId") # macrobunch - dataset = self.h5_file[dataset_key] - - if slice_: - slice_index = self._config["channels"][channel].get("slice", None) - if slice_index is not None: - dataset = np.take(dataset, slice_index, axis=1) - # If np_array is size zero, fill with NaNs - if dataset.shape[0] == 0: - # Fill the np_array with NaN values of the same shape as train_id - dataset = np.full_like(key, np.nan, dtype=np.double) - - return key, dataset - - def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: - """ - Computes the index for the 'per_electron' data. - - Args: - offset (int): The offset value. - - Returns: - tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and - the indexer. - """ - # Get the pulseId and the index_train - index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) - # Repeat the index_train by the number of pulses - index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) - # Explode the pulse dataset and subtract by the ubid_offset - pulse_ravel = dataset_pulse.ravel() - offset - # Create a MultiIndex with the index_train and the pulse - microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() - - # Only sort if necessary - indexer = slice(None) - if not microbunches.is_monotonic_increasing: - microbunches, indexer = microbunches.sort_values(return_indexer=True) - - # Count the number of electrons per microbunch and create an array of electrons - electron_counts = microbunches.value_counts(sort=False).values - electrons = np.concatenate([np.arange(count) for count in electron_counts]) - - # Final index constructed here - index = MultiIndex.from_arrays( - ( - microbunches.get_level_values(0), - microbunches.get_level_values(1).astype(int), - electrons, - ), - names=self.multi_index, - ) - return index, indexer - - @property - def df_electron(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per electron]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_electron' channel's data. - """ - offset = self._config["ubid_offset"] - # Index - index, indexer = self.pulse_index(offset) - - # Data logic - channels = get_channels(self._config["channels"], "per_electron") - slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] - - # First checking if dataset keys are the same for all channels - dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] - all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) - - # If all dataset keys are the same, we can directly use the ndarray to create frame - if all_keys_same: - _, dataset = self.get_dataset_array(channels[0]) - data_dict = { - channel: dataset[:, slice_, :].ravel() - for channel, slice_ in zip(channels, slice_index) - } - dataframe = DataFrame(data_dict) - # Otherwise, we need to create a Series for each channel and concatenate them - else: - series = { - channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) - for channel in channels - } - dataframe = concat(series, axis=1) - - drop_vals = np.arange(-offset, 0) - - # Few things happen here: - # Drop all NaN values like while creating the multiindex - # if necessary, the data is sorted with [indexer] - # MultiIndex is set - # Finally, the offset values are dropped - return ( - dataframe.dropna() - .iloc[indexer] - .set_index(index) - .drop(index=drop_vals, level="pulseId", errors="ignore") - ) - - @property - def df_pulse(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per pulse]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. - """ - series = [] - channels = get_channels(self._config["channels"], "per_pulse") - for channel in channels: - # get slice - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, np.arange(0, dataset.shape[1]), [0]), - names=self.multi_index, - ) - series.append(Series(dataset[()].ravel(), index=index, name=channel)) - - return concat(series, axis=1) # much faster when concatenating similarly indexed data first - - @property - def df_train(self) -> DataFrame: - """ - Returns a pandas DataFrame for a given channel name of type [per train]. - - Returns: - DataFrame: The pandas DataFrame for the 'per_train' channel's data. - """ - series = [] - - channels = get_channels(self._config["channels"], "per_train") - - for channel in channels: - key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( - (key, [0], [0]), - names=self.multi_index, - ) - if channel == "dldAux": - aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() - for name, slice_aux in aux_channels: - series.append(Series(dataset[: key.size, slice_aux], index, name=name)) - else: - series.append(Series(dataset, index, name=channel)) - - return concat(series, axis=1) - - def validate_channel_keys(self) -> None: - """ - Validates if the index and dataset keys for all channels in config exist in the h5 file. - - Raises: - KeyError: If the index or dataset keys do not exist in the file. - """ - for channel in self._config["channels"]: - index_key, dataset_key = self.get_index_dataset_key(channel) - if index_key not in self.h5_file: - raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") - if dataset_key not in self.h5_file: - raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") - - @property - def df(self) -> DataFrame: - """ - Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, - returning a single dataframe. - - Returns: - DataFrame: The combined pandas DataFrame. - """ - - self.validate_channel_keys() - return ( - self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") - .join(self.df_train, on=self.multi_index, how="outer") - .sort_index() - ) - - -class BufferHandler: - """ - A class for handling the creation and manipulation of buffer files using DataFrameCreator. - """ - - def __init__( - self, - config: dict, - ) -> None: - """ - Initializes the BufferHandler. - - Args: - config (dict): The configuration dictionary. - """ - self._config = config["dataframe"] - self.n_cores = config["core"].get("num_cores", os.cpu_count() - 1) - - self.buffer_paths: list[Path] = [] - self.missing_h5_files: list[Path] = [] - self.save_paths: list[Path] = [] - - self.df_electron: dd.DataFrame = None - self.df_pulse: dd.DataFrame = None - self.metadata: dict = {} - - def _schema_check(self) -> None: - """ - Checks the schema of the Parquet files. - - Raises: - ValueError: If the schema of the Parquet files does not match the configuration. - """ - existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] - parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] - config_schema = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) - - for i, schema in enumerate(parquet_schemas): - if self._config["sector_id_column"] in schema.names: - config_schema.add( - self._config["sector_id_column"], - ) # for compatibility with old files - schema_set = set(schema.names) - if schema_set != config_schema: - missing_in_parquet = config_schema - schema_set - missing_in_config = schema_set - config_schema - - missing_in_parquet_str = ( - f"Missing in parquet: {missing_in_parquet}" if missing_in_parquet else "" - ) - missing_in_config_str = ( - f"Missing in config: {missing_in_config}" if missing_in_config else "" - ) - - raise ValueError( - "The available channels do not match the schema of file", - f"{existing_parquet_filenames[i]}", - f"{missing_in_parquet_str}", - f"{missing_in_config_str}", - "Please check the configuration file or set force_recreate to True.", - ) - - def _get_files_to_read( - self, - h5_paths: list[Path], - folder: Path, - prefix: str, - suffix: str, - force_recreate: bool, - ) -> None: - """ - Determines the list of files to read and the corresponding buffer files to create. - - Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - force_recreate (bool): Flag to force recreation of buffer files. - """ - # Getting the paths of the buffer files, with subfolder as buffer and no extension - self.buffer_paths = initialize_paths( - filenames=[Path(h5_path).stem for h5_path in h5_paths], - folder=folder, - subfolder="buffer", - prefix=prefix, - suffix=suffix, - extension="", - ) - # read only the files that do not exist or if force_recreate is True - files_to_read = [ - force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths - ] - - # Get the list of H5 files to read and the corresponding buffer files to create - self.missing_h5_files = list(compress(h5_paths, files_to_read)) - self.save_paths = list(compress(self.buffer_paths, files_to_read)) - - print(f"Reading files: {len(self.missing_h5_files)} new files of {len(h5_paths)} total.") - - def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: - """ - Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. - - Args: - h5_path (Path): Path to the H5 file. - parquet_path (Path): Path to the buffer file. - """ - # Open the h5 file in read mode - h5_file = h5py.File(h5_path, "r") - - # Create a DataFrameCreator instance with the configuration and the h5 file - dfc = DataFrameCreator(config_dataframe=self._config, h5_file=h5_file) - - # Get the DataFrame from the DataFrameCreator instance - df = dfc.df - - # Close the h5 file - h5_file.close() - - # Reset the index of the DataFrame and save it as a parquet file - df.reset_index().to_parquet(parquet_path) - - def _save_buffer_files(self, debug: bool) -> None: - """ - Creates the buffer files. - - Args: - debug (bool): Flag to enable debug mode, which serializes the creation. - """ - n_cores = min(len(self.missing_h5_files), self.n_cores) - if n_cores > 0: - if debug: - for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): - self._save_buffer_file(h5_path, parquet_path) - else: - Parallel(n_jobs=n_cores, verbose=10)( - delayed(self._save_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) - ) - - def _fill_dataframes(self): - """ - Reads all parquet files into one dataframe using dask and fills NaN values. - """ - dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) - file_metadata = get_parquet_metadata(self.buffer_paths) - - channels: list[str] = get_channels( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) - index: list[str] = get_channels(index=True) - overlap = min(file["num_rows"] for file in file_metadata.values()) - self.metadata["buffer_file_metadata"] = file_metadata - dataframe = forward_fill_lazy( - df=dataframe, - columns=channels, - before=overlap, - iterations=self._config.get("forward_fill_iterations", 2), - ) - self.metadata["forward_fill"] = { - "columns": channels, - "overlap": overlap, - "iterations": self._config.get("forward_fill_iterations", 2), - } - - # Drop rows with nan values in the tof column - tof_column = self._config.get("tof_column", "dldTimeSteps") - df_electron = dataframe.dropna(subset=tof_column) - - # Set the dtypes of the channels here as there should be no null values - channel_dtypes = get_channels(self._config["channels"], "all") - config_channels = self._config["channels"] - dtypes = { - channel: config_channels[channel].get("dtype") - for channel in channel_dtypes - if config_channels[channel].get("dtype") is not None - } - - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - df_electron, meta = split_dld_time_from_sector_id( - df_electron, - config=self._config, - ) - self.metadata.update(meta) - - self.df_electron = df_electron.astype(dtypes) - self.df_pulse = dataframe[index + channels] - - def run( - self, - h5_paths: list[Path], - folder: Path, - force_recreate: bool = False, - prefix: str = "", - suffix: str = "", - debug: bool = False, - ) -> None: - """ - Runs the buffer file creation process. - - Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - force_recreate (bool): Flag to force recreation of buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - debug (bool): Flag to enable debug mode.): - """ - - self._get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) - - if not force_recreate: - self._schema_check() - - self._save_buffer_files(debug) - - self._fill_dataframes() - - -def wespe_convert(df: dd.DataFrame, df_timed: dd.DataFrame) -> tuple[dd.DataFrame, dd.DataFrame]: - df - df_timed - raise NotImplementedError("This function is not implemented yet.") - - LOADER = FlashLoader From 08f8f137c0b06436333ae441d02174dd902c4231 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 18:27:50 +0200 Subject: [PATCH 067/300] add time_elapsed method --- sed/config/flash_example_config.yaml | 4 +- sed/loader/flash/loader.py | 97 ++++++++++++++------ sed/loader/utils.py | 56 +++++++++-- tests/loader/flash/test_buffer_handler.py | 2 +- tests/loader/flash/test_dataframe_creator.py | 2 +- tests/loader/flash/test_flash_loader.py | 23 ++--- 6 files changed, 128 insertions(+), 56 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index dfb070d7..bfdf0cd3 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -54,18 +54,20 @@ dataframe: tof_ns_column: dldTime # dataframe column containing corrected time-of-flight data corrected_tof_column: "tm" + # the time stamp column + time_stamp_alias: timeStamp # time length of a base time-of-flight bin in seconds tof_binwidth: 2.0576131995767355E-11 # binning parameter for time-of-flight data. 2**tof_binning bins per base bin tof_binning: 3 # power of 2, 3 means 8 bins per step # dataframe column containing sector ID. obtained from dldTimeSteps column sector_id_column: dldSectorID - sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] # the delay stage column delay_column: delayStage # the corrected pump-probe time axis corrected_delay_column: pumpProbeTime + # the columns to be used for jitter correction jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] units: diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index b1939b02..0d0f1324 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -109,7 +109,7 @@ def _initialize_dirs(self) -> tuple[str, str]: def get_files_from_run_id( # type: ignore[override] self, - run_id: str, + run_id: str | int, folders: str | Sequence[str] = None, extension: str = "h5", ) -> list[str]: @@ -162,31 +162,6 @@ def get_files_from_run_id( # type: ignore[override] # Return the list of found files return [str(file.resolve()) for file in files] - def get_files_from_run_ids( - self, - runs: str | int | Sequence[str | int], - ) -> tuple[list[str], list[str]]: - """ - Retrieves the file paths associated with a given set of run IDs. - - Args: - runs (str | int | Sequence[str | int]): A single run ID or a list of run IDs. - - Returns: - Tuple[List[str], List[str]]: List of runs and List of file paths. - """ - files = [] - if isinstance(runs, (str, int)): - runs = [runs] - runs_ = list(map(str, runs)) # convert all elements in runs to string - for run in runs_: - run_files = self.get_files_from_run_id( - run_id=run, - folders=self.raw_dir, - ) - files.extend(run_files) - return runs_, files - def parse_metadata(self, scicat_token: str = None) -> dict: """Uses the MetadataRetriever class to fetch metadata from scicat for each run. @@ -210,8 +185,62 @@ def get_count_rate( ): return None, None - def get_elapsed_time(self, fids=None, **kwds): # noqa: ARG002 - return None + def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 + """ + Calculates the elapsed time. + + Args: + fids (Sequence[int]): A sequence of file IDs. Defaults to all files. + **kwds: + - runs: A sequence of run IDs. Takes precedence over fids. + - aggregate: Whether to return the sum of the elapsed times across + the specified files or the elapsed time for each file. Defaults to True. + + Returns: + Union[float, List[float]]: The elapsed time(s) in seconds. + + Raises: + KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. + """ + try: + file_statistics = self.metadata.get("file_statistics") + except KeyError as exc: + raise KeyError( + "File statistics missing. Use 'read_dataframe' first.", + ) from exc + + runs = kwds.get("runs", None) + + def get_elapsed_time_from_fid(fid): + try: + time_stamps = file_statistics[fid]["time_stamps"] + elapsed_time = max(time_stamps) - min(time_stamps) + except KeyError as exc: + raise KeyError( + f"Timestamp metadata missing in file {fid}." + "Add timestamp column and alias to config before loading.", + ) from exc + + return elapsed_time + + def get_elapsed_time_from_run(run_id): + files = self.get_files_from_run_id(run_id=run_id, folders=self.raw_dir) + fids = [self.files.index(file) for file in files] + return sum(get_elapsed_time_from_fid(fid) for fid in fids) + + elapsed_times = [] + if runs is not None: + elapsed_times = [get_elapsed_time_from_run(run) for run in runs] + + if fids is None: + fids = range(len(self.files)) + + elapsed_times = [get_elapsed_time_from_fid(fid) for fid in fids] + + if kwds.get("aggregate", True): + elapsed_times = sum(elapsed_times) + + return elapsed_times def read_dataframe( self, @@ -255,8 +284,16 @@ def read_dataframe( self._metadata.update(metadata) # Prepare a list of names for the runs to read and parquets to write if runs is not None: - runs, files = self.get_files_from_run_ids(runs) - self.runs = runs + files = [] + if isinstance(runs, (str, int)): + runs_ = [runs] + for run in runs_: + run_files = self.get_files_from_run_id( + run_id=run, + folders=self.raw_dir, + ) + files.extend(run_files) + self.runs = list(map(str, runs_)) super().read_dataframe(files=files, ftype=ftype) else: # This call takes care of files and folders. As we have converted runs into files diff --git a/sed/loader/utils.py b/sed/loader/utils.py index f3656866..14ae3a90 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -208,21 +208,61 @@ def split_dld_time_from_sector_id( return df, {"split_dld_time_from_sector_id": metadata} -def get_parquet_metadata(file_paths: List[Path]) -> Dict[str, Dict]: +def get_timestamp_stats(meta: pq.FileMetaData, time_stamp_col: str) -> Tuple[int, int]: + """ + Extracts the minimum and maximum timestamps from the metadata of a Parquet file. + + Args: + meta (pq.FileMetaData): The metadata of the Parquet file. + time_stamp_col (str): The name of the column containing the timestamps. + + Returns: + Tuple[int, int]: The minimum and maximum timestamps. + """ + idx = meta.schema.names.index(time_stamp_col) + timestamps = [] + for i in range(meta.num_row_groups): + stats = meta.row_group(i).column(idx).statistics + timestamps.append(stats.min) + timestamps.append(stats.max) + + return min(timestamps), max(timestamps) + + +def get_parquet_metadata(file_paths: List[Path], time_stamp_col: str) -> Dict[int, Dict]: + """ + Extracts and organizes metadata from a list of Parquet files. + + For each file, the function reads the metadata, adds the filename, and attempts to + extract the minimum and maximum timestamps. "row_groups" entry is removed from FileMetaData. + + Args: + file_paths (List[Path]): A list of paths to the Parquet files. + time_stamp_col (str): The name of the column containing the timestamps. + + Returns: + Dict[str, Dict]: A dictionary file index as key and the values as metadata of each file. + """ organized_metadata = {} for i, file_path in enumerate(file_paths): # Read the metadata for the file - file_meta = pq.read_metadata(file_path) - - # Convert the metadata to a dictionary and remove "row_groups" - # as they contain a lot of info that is not needed + file_meta: pq.FileMetaData = pq.read_metadata(file_path) + # Convert the metadata to a dictionary metadata_dict = file_meta.to_dict() - metadata_dict.pop("row_groups", None) - # Add the filename to the metadata dictionary metadata_dict["filename"] = str(file_path.name) + # Get the timestamp min and max + try: + timestamps = get_timestamp_stats(file_meta, time_stamp_col) + metadata_dict["time_stamps"] = timestamps + except ValueError: + pass + + # Remove "row_groups" as they contain a lot of info that is not needed + metadata_dict.pop("row_groups", None) + # Add the metadata dictionary to the organized_metadata dictionary - organized_metadata[f"file_{i + 1}"] = metadata_dict + organized_metadata[i] = metadata_dict return organized_metadata diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index be59c59b..a5d103b4 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -4,7 +4,7 @@ import pandas as pd import pytest -from sed.loader.flash.loader import BufferHandler +from sed.loader.flash.buffer_handler import BufferHandler from sed.loader.flash.utils import get_channels diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index b9c1b834..13ecb851 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -6,7 +6,7 @@ from pandas import Index from pandas import MultiIndex -from sed.loader.flash.loader import DataFrameCreator +from sed.loader.flash.dataframe import DataFrameCreator from sed.loader.flash.utils import get_channels diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index bce97e6b..18c3529c 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import Literal -import pandas as pd import pytest from sed.core.config import parse_config @@ -61,17 +60,17 @@ def test_initialize_dirs( # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) - data_raw_dir, data_parquet_dir = fl.initialize_dirs() + data_raw_dir, data_parquet_dir = fl._initialize_dirs() - assert expected_raw_path == data_raw_dir[0] - assert expected_processed_path == data_parquet_dir + assert str(expected_raw_path) == data_raw_dir + assert str(expected_processed_path) == data_parquet_dir # remove breamtimeid, year and daq from config to raise error del config["core"]["beamtime_id"] fl = FlashLoader(config=config) with pytest.raises(ValueError) as e: - _, _ = fl.initialize_dirs() - + _, _ = fl._initialize_dirs() + print(e.value) assert "The beamtime_id and year are required." in str(e.value) @@ -88,7 +87,7 @@ def test_initialize_dirs_filenotfound(config_file: dict) -> None: # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = fl.initialize_dirs() + _, _ = fl._initialize_dirs() def test_save_read_parquet_flash(config): @@ -98,12 +97,6 @@ def test_save_read_parquet_flash(config): config_["core"]["paths"]["data_parquet_dir"] + "_flash_save_read/" ) fl = FlashLoader(config=config_) - df1, _, _ = fl.read_dataframe(runs=[43878, 43879], save_parquet=True) - - df2, _, _ = fl.read_dataframe(runs=[43878, 43879], load_parquet=True) + df1, _, _ = fl.read_dataframe(runs=[43878, 43879]) - # check if parquet read is same as parquet saved read correctly - pd.testing.assert_frame_equal( - df1.compute().reset_index(drop=True), - df2.compute().reset_index(drop=True), - ) + df2, _, _ = fl.read_dataframe(runs=[43878, 43879]) From fa68746713d9431451730c9863b5bf8c2818549a Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 21:20:56 +0200 Subject: [PATCH 068/300] fix test issues --- sed/loader/flash/buffer_handler.py | 5 +++-- sed/loader/flash/loader.py | 7 +++---- tests/loader/flash/conftest.py | 5 ++++- tests/loader/flash/test_buffer_handler.py | 12 ++++++------ tests/loader/flash/test_flash_loader.py | 10 ++++------ 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 99f2a2e7..ade06975 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -76,7 +76,8 @@ def _schema_check(self) -> None: raise ValueError( f"The available channels do not match the schema of file {filename}. " - f"{' '.join(errors)}", + f"{' '.join(errors)}. " + "Please check the configuration file or set force_recreate to True.", ) def _get_files_to_read( @@ -165,7 +166,7 @@ def _fill_dataframes(self): dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) file_metadata = get_parquet_metadata( self.buffer_paths, - time_stamp_col=self._config["time_stamp_alias", "timeStamp"], + time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), ) self.metadata["file_statistics"] = file_metadata diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 0d0f1324..844a6d7c 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -246,7 +246,7 @@ def read_dataframe( self, files: str | Sequence[str] = None, folders: str | Sequence[str] = None, - runs: str | int | Sequence[str] | Sequence[int] = None, + runs: str | int | Sequence[str | int] = None, ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, @@ -285,15 +285,14 @@ def read_dataframe( # Prepare a list of names for the runs to read and parquets to write if runs is not None: files = [] - if isinstance(runs, (str, int)): - runs_ = [runs] + runs_ = [str(runs)] if isinstance(runs, (str, int)) else list(map(str, runs)) for run in runs_: run_files = self.get_files_from_run_id( run_id=run, folders=self.raw_dir, ) files.extend(run_files) - self.runs = list(map(str, runs_)) + self.runs = runs_ super().read_dataframe(files=files, ftype=ftype) else: # This call takes care of files and folders. As we have converted runs into files diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 9b7dda03..9c27a216 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -3,6 +3,7 @@ import os import shutil from importlib.util import find_spec +from pathlib import Path import h5py import pytest @@ -68,7 +69,9 @@ def fixture_h5_paths(): Returns: list: A list of h5 file paths. """ - return [os.path.join(package_dir, f"../tests/data/loader/flash/{path}") for path in H5_PATHS] + return [ + Path(os.path.join(package_dir, f"../tests/data/loader/flash/{path}")) for path in H5_PATHS + ] # @pytest.fixture(name="pulserSignAdc_channel_array") diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index a5d103b4..eb7666c9 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -82,12 +82,12 @@ def test_buffer_schema_mismatch(config, h5_paths): with pytest.raises(ValueError) as e: bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, debug=True) - expected_error = e.value.args + expected_error = e.value.args[0] # Validate the specific error messages for schema mismatch - assert "The available channels do not match the schema of file" in expected_error[0] - assert expected_error[2] == "Missing in parquet: {'gmdTunnel2'}" - assert expected_error[4] == "Please check the configuration file or set force_recreate to True." + assert "The available channels do not match the schema of file" in expected_error + assert "Missing in parquet: {'gmdTunnel2'}" in expected_error + assert "Please check the configuration file or set force_recreate to True." in expected_error # Force recreation of the dataframe, including the added channel 'gmdTunnel2' bh = BufferHandler(config) @@ -101,9 +101,9 @@ def test_buffer_schema_mismatch(config, h5_paths): bh = BufferHandler(config) bh.run(h5_paths=h5_paths, folder=folder, debug=True) - expected_error = e.value.args + expected_error = e.value.args[0] # Check for the specific error message indicating a missing channel in the configuration - assert expected_error[3] == "Missing in config: {'gmdTunnel2'}" + assert "Missing in config: {'gmdTunnel2'}" in expected_error # Clean up created buffer files after the test [path.unlink() for path in bh.buffer_paths] diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 18c3529c..84045beb 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -60,16 +60,14 @@ def test_initialize_dirs( # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) - data_raw_dir, data_parquet_dir = fl._initialize_dirs() - assert str(expected_raw_path) == data_raw_dir - assert str(expected_processed_path) == data_parquet_dir + assert str(expected_raw_path) == fl.raw_dir + assert str(expected_processed_path) == fl.parquet_dir # remove breamtimeid, year and daq from config to raise error del config["core"]["beamtime_id"] - fl = FlashLoader(config=config) with pytest.raises(ValueError) as e: - _, _ = fl._initialize_dirs() + fl = FlashLoader(config=config) print(e.value) assert "The beamtime_id and year are required." in str(e.value) @@ -85,8 +83,8 @@ def test_initialize_dirs_filenotfound(config_file: dict) -> None: config["core"]["year"] = "2000" # Instance of class with correct config and call initialize_dirs - fl = FlashLoader(config=config) with pytest.raises(FileNotFoundError): + fl = FlashLoader(config=config) _, _ = fl._initialize_dirs() From cb884dd3afffea1e220e8297e87f26c44002ee95 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 22:11:44 +0200 Subject: [PATCH 069/300] add tests for elapsed time --- sed/loader/flash/loader.py | 18 +++---- tests/loader/flash/test_flash_loader.py | 71 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 844a6d7c..4cc4a810 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -173,7 +173,7 @@ def parse_metadata(self, scicat_token: str = None) -> dict: metadata = metadata_retriever.get_metadata( beamtime_id=self._config["core"]["beamtime_id"], runs=self.runs, - metadata=self.metadata, + metadata=self._metadata, ) return metadata @@ -203,14 +203,12 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. """ try: - file_statistics = self.metadata.get("file_statistics") - except KeyError as exc: + file_statistics = self._metadata["file_statistics"] + except Exception as exc: raise KeyError( "File statistics missing. Use 'read_dataframe' first.", ) from exc - runs = kwds.get("runs", None) - def get_elapsed_time_from_fid(fid): try: time_stamps = file_statistics[fid]["time_stamps"] @@ -229,13 +227,13 @@ def get_elapsed_time_from_run(run_id): return sum(get_elapsed_time_from_fid(fid) for fid in fids) elapsed_times = [] + runs = kwds.get("runs") if runs is not None: elapsed_times = [get_elapsed_time_from_run(run) for run in runs] - - if fids is None: - fids = range(len(self.files)) - - elapsed_times = [get_elapsed_time_from_fid(fid) for fid in fids] + else: + if fids is None: + fids = range(len(self.files)) + elapsed_times = [get_elapsed_time_from_fid(fid) for fid in fids] if kwds.get("aggregate", True): elapsed_times = sum(elapsed_times) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 84045beb..ca3fa415 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -98,3 +98,74 @@ def test_save_read_parquet_flash(config): df1, _, _ = fl.read_dataframe(runs=[43878, 43879]) df2, _, _ = fl.read_dataframe(runs=[43878, 43879]) + + +def test_get_elapsed_time_fid(config_file): + """Test get_elapsed_time method of FlashLoader class""" + # Create an instance of FlashLoader + fl = FlashLoader(config=config_file) + + # Mock the file_statistics and files + fl.metadata = { + "file_statistics": { + 0: {"time_stamps": [10, 20]}, + 1: {"time_stamps": [20, 30]}, + 2: {"time_stamps": [30, 40]}, + }, + } + fl.files = ["file0", "file1", "file2"] + + # Test get_elapsed_time with fids + assert fl.get_elapsed_time(fids=[0, 1]) == 20 + + # # Test get_elapsed_time with runs + # # Assuming get_files_from_run_id(43878) returns ["file0", "file1"] + # assert fl.get_elapsed_time(runs=[43878]) == 20 + + # Test get_elapsed_time with aggregate=False + assert fl.get_elapsed_time(fids=[0, 1], aggregate=False) == [10, 10] + + # Test KeyError when file_statistics is missing + fl.metadata = {"something": "else"} + with pytest.raises(KeyError) as e: + fl.get_elapsed_time(fids=[0, 1]) + + assert "File statistics missing. Use 'read_dataframe' first." in str(e.value) + # Test KeyError when time_stamps is missing + fl.metadata = { + "file_statistics": { + 0: {}, + 1: {"time_stamps": [20, 30]}, + }, + } + with pytest.raises(KeyError) as e: + fl.get_elapsed_time(fids=[0, 1]) + + assert "Timestamp metadata missing in file 0" in str(e.value) + + +def test_get_elapsed_time_run(config_file): + config = config_file + config["core"]["paths"] = { + "data_raw_dir": "tests/data/loader/flash/", + "data_parquet_dir": "tests/data/loader/flash/parquet/get_elapsed_time_run", + } + """Test get_elapsed_time method of FlashLoader class""" + # Create an instance of FlashLoader + fl = FlashLoader(config=config_file) + + fl.read_dataframe(runs=[43878, 43879]) + start, end = fl._metadata["file_statistics"][0]["time_stamps"] + expected_elapsed_time_0 = end - start + start, end = fl._metadata["file_statistics"][1]["time_stamps"] + expected_elapsed_time_1 = end - start + + elapsed_time = fl.get_elapsed_time(runs=[43878]) + assert elapsed_time == expected_elapsed_time_0 + + elapsed_time = fl.get_elapsed_time(runs=[43878, 43879], aggregate=False) + assert elapsed_time == [expected_elapsed_time_0, expected_elapsed_time_1] + + elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) + start, end = fl._metadata["file_statistics"][1]["time_stamps"] + assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 From ae265554da5f1e8657091f43651bd8fdd27dff4e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 22:15:27 +0200 Subject: [PATCH 070/300] fix main loader tests --- sed/loader/sxp/loader.py | 4 ++-- tests/loader/test_loaders.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index aa018b91..9c713c8a 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -53,7 +53,7 @@ def __init__(self, config: dict) -> None: self.failed_files_error: List[str] = [] self.array_indices: List[List[slice]] = None - def initialize_dirs(self) -> Tuple[List[Path], Path]: + def _initialize_dirs(self) -> Tuple[List[Path], Path]: """ Initializes the paths based on the configuration. @@ -940,7 +940,7 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self.initialize_dirs() + data_raw_dir, data_parquet_dir = self._initialize_dirs() # Prepare a list of names for the runs to read and parquets to write if runs is not None: diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 08ff15b7..cd2c6b96 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -163,7 +163,7 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -196,7 +196,7 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loaded_timed_dataframe is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -206,7 +206,7 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -240,7 +240,7 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loaded_time is None and loaded_countrate is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -251,7 +251,7 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -283,9 +283,9 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: ) elapsed_time = loader.get_elapsed_time() if elapsed_time is None: - if loader.__name__ in {"flash", "sxp"}: + if loader.__name__ in {"sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) pytest.skip("Not implemented") @@ -296,7 +296,7 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader.initialize_dirs() + _, parquet_data_dir = loader._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) From 04714bc28a9f96bef652e9c481e99fadebca10df Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 22:20:46 +0200 Subject: [PATCH 071/300] fix sxp loader tests --- tests/loader/sxp/test_sxp_loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 73d470fd..8779def9 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -99,7 +99,7 @@ def test_initialize_dirs(config_file: dict, fs) -> None: # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) - data_raw_dir, data_parquet_dir = sl.initialize_dirs() + data_raw_dir, data_parquet_dir = sl._initialize_dirs() assert expected_raw_path == data_raw_dir[0] assert expected_processed_path == data_parquet_dir @@ -118,7 +118,7 @@ def test_initialize_dirs_filenotfound(config_file: dict): # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = sl.initialize_dirs() + _, _ = sl._initialize_dirs() def test_invalid_channel_format(config_file: dict): @@ -209,6 +209,6 @@ def test_buffer_schema_mismatch(config_file: dict): assert expected_error[3] == "Missing in config: {'delayStage2'}" # Clean up created buffer files after the test - _, parquet_data_dir = sl.initialize_dirs() + _, parquet_data_dir = sl._initialize_dirs() for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) From 65895954e32032fcd95e09725ada6981fd82b7d7 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 10 Jun 2024 23:04:38 +0200 Subject: [PATCH 072/300] fix tests --- sed/loader/flash/loader.py | 27 ++++++++-------- sed/loader/sxp/loader.py | 17 +++++----- tests/loader/flash/test_flash_loader.py | 12 +++---- tests/loader/sxp/test_sxp_loader.py | 14 ++++----- tests/loader/test_loaders.py | 42 ++++++++++++------------- 5 files changed, 55 insertions(+), 57 deletions(-) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 4cc4a810..e1f74673 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -42,10 +42,10 @@ def __init__(self, config: dict) -> None: """ super().__init__(config=config) self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof - self._metadata: dict = {} - self.raw_dir, self.parquet_dir = self._initialize_dirs() + self.raw_dir: str = None + self.parquet_dir: str = None - def _initialize_dirs(self) -> tuple[str, str]: + def _initialize_dirs(self) -> None: """ Initializes the directories on Maxwell based on configuration. If paths is provided in the configuration, the raw data directory and parquet data directory are taken from there. @@ -53,10 +53,6 @@ def _initialize_dirs(self) -> tuple[str, str]: The first path that has either online- or express- prefix, or the daq name is taken as the raw data directory. - Returns: - Tuple[str, str]: A tuple containing raw data directory - and the parquet data directory. - Raises: ValueError: If required values are missing from the configuration. FileNotFoundError: If the raw data directories are not found. @@ -105,7 +101,8 @@ def _initialize_dirs(self) -> tuple[str, str]: data_parquet_dir.mkdir(parents=True, exist_ok=True) - return str(data_raw_dir[0].resolve()), str(data_parquet_dir) + self.raw_dir = str(data_raw_dir[0].resolve()) + self.parquet_dir = str(data_parquet_dir) def get_files_from_run_id( # type: ignore[override] self, @@ -173,7 +170,7 @@ def parse_metadata(self, scicat_token: str = None) -> dict: metadata = metadata_retriever.get_metadata( beamtime_id=self._config["core"]["beamtime_id"], runs=self.runs, - metadata=self._metadata, + metadata=self.metadata, ) return metadata @@ -203,7 +200,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. """ try: - file_statistics = self._metadata["file_statistics"] + file_statistics = self.metadata["file_statistics"] except Exception as exc: raise KeyError( "File statistics missing. Use 'read_dataframe' first.", @@ -222,6 +219,8 @@ def get_elapsed_time_from_fid(fid): return elapsed_time def get_elapsed_time_from_run(run_id): + if self.raw_dir is None: + self._initialize_dirs() files = self.get_files_from_run_id(run_id=run_id, folders=self.raw_dir) fids = [self.files.index(file) for file in files] return sum(get_elapsed_time_from_fid(fid) for fid in fids) @@ -279,7 +278,7 @@ def read_dataframe( """ t0 = time.time() - self._metadata.update(metadata) + self._initialize_dirs() # Prepare a list of names for the runs to read and parquets to write if runs is not None: files = [] @@ -326,12 +325,12 @@ def read_dataframe( if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) - self._metadata.update(self.parse_metadata(**kwds) if collect_metadata else {}) - self._metadata.update(bh.metadata) + self.metadata.update(self.parse_metadata(**kwds) if collect_metadata else {}) + self.metadata.update(bh.metadata) print(f"loading complete in {time.time() - t0: .2f} s") - return df, df_timed, self._metadata + return df, df_timed, self.metadata LOADER = FlashLoader diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 9c713c8a..097f37b2 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -52,15 +52,13 @@ def __init__(self, config: dict) -> None: self.index_per_pulse: MultiIndex = None self.failed_files_error: List[str] = [] self.array_indices: List[List[slice]] = None + self.raw_dir: str = None + self.parquet_dir: str = None - def _initialize_dirs(self) -> Tuple[List[Path], Path]: + def _initialize_dirs(self): """ Initializes the paths based on the configuration. - Returns: - Tuple[List[Path], Path]: A tuple containing a list of raw data directories - paths and the parquet data directory path. - Raises: ValueError: If required values are missing from the configuration. FileNotFoundError: If the raw data directories are not found. @@ -102,7 +100,8 @@ def _initialize_dirs(self) -> Tuple[List[Path], Path]: data_parquet_dir.mkdir(parents=True, exist_ok=True) - return data_raw_dir, data_parquet_dir + self.raw_dir = data_raw_dir + self.parquet_dir = data_parquet_dir def get_files_from_run_id( self, @@ -940,7 +939,7 @@ def read_dataframe( """ t0 = time.time() - data_raw_dir, data_parquet_dir = self._initialize_dirs() + self._initialize_dirs() # Prepare a list of names for the runs to read and parquets to write if runs is not None: @@ -950,7 +949,7 @@ def read_dataframe( for run in runs: run_files = self.get_files_from_run_id( run_id=run, - folders=[str(folder.resolve()) for folder in data_raw_dir], + folders=[str(Path(folder).resolve()) for folder in self.raw_dir], extension=ftype, daq=self._config["dataframe"]["daq"], ) @@ -968,7 +967,7 @@ def read_dataframe( metadata=metadata, ) - df, df_timed = self.parquet_handler(data_parquet_dir, **kwds) + df, df_timed = self.parquet_handler(Path(self.parquet_dir), **kwds) if collect_metadata: metadata = self.gather_metadata( diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index ca3fa415..2e3d4049 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -60,14 +60,14 @@ def test_initialize_dirs( # Instance of class with correct config and call initialize_dirs fl = FlashLoader(config=config) - + fl._initialize_dirs() assert str(expected_raw_path) == fl.raw_dir assert str(expected_processed_path) == fl.parquet_dir # remove breamtimeid, year and daq from config to raise error del config["core"]["beamtime_id"] with pytest.raises(ValueError) as e: - fl = FlashLoader(config=config) + fl._initialize_dirs() print(e.value) assert "The beamtime_id and year are required." in str(e.value) @@ -85,7 +85,7 @@ def test_initialize_dirs_filenotfound(config_file: dict) -> None: # Instance of class with correct config and call initialize_dirs with pytest.raises(FileNotFoundError): fl = FlashLoader(config=config) - _, _ = fl._initialize_dirs() + fl._initialize_dirs() def test_save_read_parquet_flash(config): @@ -155,9 +155,9 @@ def test_get_elapsed_time_run(config_file): fl = FlashLoader(config=config_file) fl.read_dataframe(runs=[43878, 43879]) - start, end = fl._metadata["file_statistics"][0]["time_stamps"] + start, end = fl.metadata["file_statistics"][0]["time_stamps"] expected_elapsed_time_0 = end - start - start, end = fl._metadata["file_statistics"][1]["time_stamps"] + start, end = fl.metadata["file_statistics"][1]["time_stamps"] expected_elapsed_time_1 = end - start elapsed_time = fl.get_elapsed_time(runs=[43878]) @@ -167,5 +167,5 @@ def test_get_elapsed_time_run(config_file): assert elapsed_time == [expected_elapsed_time_0, expected_elapsed_time_1] elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) - start, end = fl._metadata["file_statistics"][1]["time_stamps"] + start, end = fl.metadata["file_statistics"][1]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 8779def9..18dc7eb9 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -99,10 +99,10 @@ def test_initialize_dirs(config_file: dict, fs) -> None: # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) - data_raw_dir, data_parquet_dir = sl._initialize_dirs() + sl._initialize_dirs() - assert expected_raw_path == data_raw_dir[0] - assert expected_processed_path == data_parquet_dir + assert expected_raw_path == sl.raw_dir[0] + assert expected_processed_path == sl.parquet_dir def test_initialize_dirs_filenotfound(config_file: dict): @@ -118,7 +118,7 @@ def test_initialize_dirs_filenotfound(config_file: dict): # Instance of class with correct config and call initialize_dirs sl = SXPLoader(config=config) with pytest.raises(FileNotFoundError): - _, _ = sl._initialize_dirs() + sl._initialize_dirs() def test_invalid_channel_format(config_file: dict): @@ -209,6 +209,6 @@ def test_buffer_schema_mismatch(config_file: dict): assert expected_error[3] == "Missing in config: {'delayStage2'}" # Clean up created buffer files after the test - _, parquet_data_dir = sl._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + sl._initialize_dirs() + for file in os.listdir(Path(sl.parquet_dir, "buffer")): + os.remove(Path(sl.parquet_dir, "buffer", file)) diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index cd2c6b96..0add9f34 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -163,9 +163,9 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -196,9 +196,9 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loaded_timed_dataframe is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) pytest.skip("Not implemented") assert isinstance(loaded_timed_dataframe, ddf.DataFrame) assert set(loaded_timed_dataframe.columns).issubset(set(loaded_dataframe.columns)) @@ -206,9 +206,9 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -240,9 +240,9 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loaded_time is None and loaded_countrate is None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) pytest.skip("Not implemented") assert len(loaded_time) == len(loaded_countrate) loaded_time2, loaded_countrate2 = loader.get_count_rate(fids=[0]) @@ -251,9 +251,9 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -285,9 +285,9 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if elapsed_time is None: if loader.__name__ in {"sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) pytest.skip("Not implemented") assert elapsed_time > 0 elapsed_time2 = loader.get_elapsed_time(fids=[0]) @@ -296,9 +296,9 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) - _, parquet_data_dir = loader._initialize_dirs() - for file in os.listdir(Path(parquet_data_dir, "buffer")): - os.remove(Path(parquet_data_dir, "buffer", file)) + loader._initialize_dirs() + for file in os.listdir(Path(loader.parquet_dir, "buffer")): + os.remove(Path(loader.parquet_dir, "buffer", file)) def test_mpes_timestamps() -> None: From 1b73b76892c823978000322bfda79741573b16ff Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 12 Jun 2024 12:00:47 +0200 Subject: [PATCH 073/300] fix minor issue with repr html --- sed/core/metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sed/core/metadata.py b/sed/core/metadata.py index 155bdfce..73cd825f 100644 --- a/sed/core/metadata.py +++ b/sed/core/metadata.py @@ -25,6 +25,8 @@ def _format_attributes(self, attributes, indent=0): INDENT_FACTOR = 20 html = "" for key, value in attributes.items(): + # Ensure the key is a string + key = str(key) # Format key formatted_key = key.replace("_", " ").title() formatted_key = f"{formatted_key}" From 852a86701101b582d5ccb7e7f55d19896ad2da9b Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 12 Jun 2024 13:33:44 +0200 Subject: [PATCH 074/300] add available runs property --- sed/loader/flash/loader.py | 16 ++++++++++++++++ tests/loader/flash/test_flash_loader.py | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index e1f74673..600c6697 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -9,6 +9,7 @@ """ from __future__ import annotations +import re import time from pathlib import Path from typing import Sequence @@ -104,6 +105,21 @@ def _initialize_dirs(self) -> None: self.raw_dir = str(data_raw_dir[0].resolve()) self.parquet_dir = str(data_parquet_dir) + @property + def available_runs(self) -> list[int]: + # Get all files in raw_dir with "run" in their names + files = list(Path(self.raw_dir).glob("*run*")) + + # Extract run IDs from filenames + run_ids = set() + for file in files: + match = re.search(r"run(\d+)", file.name) + if match: + run_ids.add(int(match.group(1))) + + # Return run IDs in sorted order + return sorted(list(run_ids)) + def get_files_from_run_id( # type: ignore[override] self, run_id: str | int, diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 2e3d4049..000a36d3 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -169,3 +169,27 @@ def test_get_elapsed_time_run(config_file): elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) start, end = fl.metadata["file_statistics"][1]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 + + +def test_available_runs(monkeypatch, config_file): + """Test available_runs property of FlashLoader class""" + # Create an instance of FlashLoader + fl = FlashLoader(config=config_file) + + # Mock the raw_dir and files + fl.raw_dir = "/path/to/raw_dir" + files = [ + "run1_file1.h5", + "run3_file1.h5", + "run2_file1.h5", + "run1_file2.h5", + ] + + # Mock the glob method to return the mock files + def mock_glob(*args, **kwargs): # noqa: ARG001 + return [Path(fl.raw_dir, file) for file in files] + + monkeypatch.setattr(Path, "glob", mock_glob) + + # Test available_runs + assert fl.available_runs == [1, 2, 3] From b456cba97a526cc5c2ee67ba3fd6cb4de03a650e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 20 Jun 2024 17:49:37 +0200 Subject: [PATCH 075/300] fix some linting errors --- sed/calibrator/energy.py | 14 +++++++------- sed/loader/mpes/loader.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 0724a3c1..1a9a2f0f 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -2017,7 +2017,7 @@ def peakdetect1d( # maxima and minima candidates are temporarily stored in # mx and mn respectively - _min, _max = np.Inf, -np.Inf + _min, _max = np.inf, -np.inf # Only detect peak if there is 'lookahead' amount of points after it for index, (x, y) in enumerate( @@ -2032,15 +2032,15 @@ def peakdetect1d( _min_pos = x # Find local maxima - if y < _max - delta and _max != np.Inf: + if y < _max - delta and _max != np.inf: # Maxima peak candidate found # look ahead in signal to ensure that this is a peak and not jitter if y_axis[index : index + lookahead].max() < _max: max_peaks.append([_max_pos, _max]) dump.append(True) # Set algorithm to only find minima now - _max = np.Inf - _min = np.Inf + _max = np.inf + _min = np.inf if index + lookahead >= length: # The end is within lookahead no more peaks can be found @@ -2051,15 +2051,15 @@ def peakdetect1d( # mxpos = x_axis[np.where(y_axis[index:index+lookahead]==mx)] # Find local minima - if y > _min + delta and _min != -np.Inf: + if y > _min + delta and _min != -np.inf: # Minima peak candidate found # look ahead in signal to ensure that this is a peak and not jitter if y_axis[index : index + lookahead].min() > _min: min_peaks.append([_min_pos, _min]) dump.append(False) # Set algorithm to only find maxima now - _min = -np.Inf - _max = -np.Inf + _min = -np.inf + _max = -np.inf if index + lookahead >= length: # The end is within lookahead no more peaks can be found diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 1793a300..7674251b 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -840,7 +840,7 @@ def gather_metadata( ].keys() lens_volts = np.array( - [metadata["file"].get(f"KTOF:Lens:{lens}:V", np.NaN) for lens in lens_list], + [metadata["file"].get(f"KTOF:Lens:{lens}:V", np.nan) for lens in lens_list], ) for mode, value in self._config["metadata"]["lens_mode_config"].items(): lens_volts_config = np.array([value[k] for k in lens_list]) From 9d61149577afe621f5f82ca0fdf1e75145ca02de Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 18:56:13 +0200 Subject: [PATCH 076/300] fix issues with matplotlib --- sed/calibrator/energy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 0724a3c1..a793195a 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -378,8 +378,8 @@ def update(refid, ranges): plot_segs[itr].set_ydata(traceseg) plot_segs[itr].set_xdata(tofseg) - plot_peaks[itr].set_xdata(self.peaks[itr, 0]) - plot_peaks[itr].set_ydata(self.peaks[itr, 1]) + plot_peaks[itr].set_xdata([self.peaks[itr, 0]]) + plot_peaks[itr].set_ydata([self.peaks[itr, 1]]) fig.canvas.draw_idle() @@ -1132,9 +1132,9 @@ def update(amplitude, x_center, y_center, **kwds): ) trace1.set_ydata(correction_x) - line1.set_xdata(x=x_center) + line1.set_xdata([x_center]) trace2.set_ydata(correction_y) - line2.set_xdata(x=y_center) + line2.set_xdata([y_center]) fig.canvas.draw_idle() From 7cdf158804b705d1c3c0749cdf369ca7c5fe1887 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 18:57:17 +0200 Subject: [PATCH 077/300] limit dask version and update lockfile and tests --- poetry.lock | 3512 ++++++++++++++----------------------------- pyproject.toml | 5 +- tests/test_dfops.py | 4 - 3 files changed, 1157 insertions(+), 2364 deletions(-) diff --git a/poetry.lock b/poetry.lock index 14ca638b..6c494a47 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiofiles" @@ -61,6 +61,20 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] +[[package]] +name = "anytree" +version = "2.12.1" +description = "Powerful and Lightweight Python Tree Data Structure with various plugins" +optional = false +python-versions = ">=3.7.2,<4" +files = [ + {file = "anytree-2.12.1-py3-none-any.whl", hash = "sha256:5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0"}, + {file = "anytree-2.12.1.tar.gz", hash = "sha256:244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830"}, +] + +[package.dependencies] +six = "*" + [[package]] name = "appnope" version = "0.1.4" @@ -148,45 +162,36 @@ types-python-dateutil = ">=2.8.10" doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] -[[package]] -name = "asciitree" -version = "0.3.3" -description = "Draws ASCII trees." -optional = false -python-versions = "*" -files = [ - {file = "asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e"}, -] - [[package]] name = "ase" -version = "3.22.1" +version = "3.23.0" description = "Atomic Simulation Environment" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "ase-3.22.1-py3-none-any.whl", hash = "sha256:66cc481e3d82bea8931d5fe677119932ea0bd1bc08b901fd84f36b37fa681c9e"}, - {file = "ase-3.22.1.tar.gz", hash = "sha256:004df6b0ea04b1114c790fadfe45d4125eb0e53125c66a93425af853d82ab432"}, + {file = "ase-3.23.0-py3-none-any.whl", hash = "sha256:52060410e720b6c701ea1ebecfdeb5ec6f9c1c63edc7cee68c15bd66d226dd43"}, + {file = "ase-3.23.0.tar.gz", hash = "sha256:91a2aa31d89bd90b0efdfe4a7e84264f32828b2abfc9f38e65e041ad76fec8ae"}, ] [package.dependencies] -matplotlib = ">=3.1.0" -numpy = ">=1.15.0" -scipy = ">=1.1.0" +matplotlib = ">=3.3.4" +numpy = ">=1.18.5" +scipy = ">=1.6.0" [package.extras] docs = ["pillow", "sphinx", "sphinx-rtd-theme"] -test = ["pytest (>=5.0.0)", "pytest-mock (>=3.3.0)", "pytest-xdist (>=1.30.0)"] +spglib = ["spglib (>=1.9)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] [[package]] name = "asteval" -version = "0.9.32" +version = "0.9.33" description = "Safe, minimalistic evaluator of python expression using ast module" optional = false python-versions = ">=3.8" files = [ - {file = "asteval-0.9.32-py3-none-any.whl", hash = "sha256:4d0da45a15f15eeb88bb53cf4c352591ccb00f00f81f74649fd7084519adc3fe"}, - {file = "asteval-0.9.32.tar.gz", hash = "sha256:3bef25a973d378fda21c83a38c6292c4d0d94773f49f42073e69dbb19932bb74"}, + {file = "asteval-0.9.33-py3-none-any.whl", hash = "sha256:aae3a0308575a545c8cecc43a6632219e6a90963a56380c74632cf54311e43bf"}, + {file = "asteval-0.9.33.tar.gz", hash = "sha256:94981701f4d252c88aa5e821121b1aabef73a003da138fc6405169c9e675d24d"}, ] [package.extras] @@ -249,13 +254,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.4.8.0.32.4" +version = "0.2024.6.17.0.31.35" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" files = [ - {file = "astropy-iers-data-0.2024.4.8.0.32.4.tar.gz", hash = "sha256:420afca681a63ef2fa9fa22f78b7629008bd8638ed0a73f9c9288a08dca08bc7"}, - {file = "astropy_iers_data-0.2024.4.8.0.32.4-py3-none-any.whl", hash = "sha256:4d461830f74b7b8bedac00d45d1a5413b6db58442e2df7c38c6fdc7cabc7a553"}, + {file = "astropy_iers_data-0.2024.6.17.0.31.35-py3-none-any.whl", hash = "sha256:f4e0b40563813c4297745dd4ec03d80b2cbd6cb29340c8df0534b296cb27e3cf"}, + {file = "astropy_iers_data-0.2024.6.17.0.31.35.tar.gz", hash = "sha256:a6e0dca0985e15dfc4f3fc508bfb29b2b046b59eb9d028416860afa9c63b17eb"}, ] [package.extras] @@ -301,13 +306,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "babel" -version = "2.14.0" +version = "2.15.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] [package.extras] @@ -352,59 +357,15 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.3)"] -[[package]] -name = "blosc2" -version = "2.5.1" -description = "Python wrapper for the C-Blosc2 library" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "blosc2-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c861262b7fe317c1614a9b59b6c9edf409532b4a6aaf5b2f4ad0d79c6f800b57"}, - {file = "blosc2-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f35b5d69a7a41e9d5054297d2540c25f8af5ea3c62e4a80ca7359292d783c04"}, - {file = "blosc2-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:546fa39f397dd54b13d7c42a4f890afaf16c70fe478712070942d464c440ce03"}, - {file = "blosc2-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5455af77e7e94159bb4966cae554f232ca2d52bb80cd3f878ecef39cf569da2a"}, - {file = "blosc2-2.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4dc4f595bf95c350c50bb77a8749cdd08a5dc2bdf3bdb18983d49a52d60b595"}, - {file = "blosc2-2.5.1-cp310-cp310-win32.whl", hash = "sha256:873483bd5c6afb8d139039180ee57b74373232e87b032cb80389fd8bb883ea8e"}, - {file = "blosc2-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d5a7ef00b82fbca069e949335f9c92ce7cbe2039a9fa2e2bd4f5f418043d6262"}, - {file = "blosc2-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da826d42d616f8a939f27e1501b40e764fded66bc80177eeaefcebdbf3b3afb8"}, - {file = "blosc2-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae2e0c5dc8561a6b17842ee4320b49621434c20e622c9e9f5c67c9c6eb3b06a3"}, - {file = "blosc2-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3cab9c12a4364c643266ee7d9583b526c0f484a291d72ec6efb09ea7ffbbf9"}, - {file = "blosc2-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f03a723130cf07e4309fe34b1360c868f4376e862f8ff664eb40d019fdd3f6"}, - {file = "blosc2-2.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0fd109eef815ea1e50fde4f676388aa2f3bb5543502d125fb63f16ec7a014464"}, - {file = "blosc2-2.5.1-cp311-cp311-win32.whl", hash = "sha256:1a3edc3256bad04d3db30c9de7eac3a820f96e741fc754cdabb6a9991e5c37e8"}, - {file = "blosc2-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:e7499e277c13334d54f84e74f429f32341f99f7b978deaf9a7c2e963904cb48c"}, - {file = "blosc2-2.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ab849d3adaeb035f2f16cf495cff1792b28d58dfb3de21b9459ee355c6bb8df3"}, - {file = "blosc2-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd66e60dafcc93d4c1f815d726d76f9fb067ecc9106a6c661010e709135c79ce"}, - {file = "blosc2-2.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb5fcd1775b3884d9825aa51fb45253f45cfa21c77f4135fad5dc5db710c2a34"}, - {file = "blosc2-2.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19f79071a336fcf1eda01cd0171291a4ab82b16cf9a15d2b4d26c010146f13b5"}, - {file = "blosc2-2.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:956a63231f1b448803e9b4bc3e704ea424c89fc14418d99093472c74f19c19e1"}, - {file = "blosc2-2.5.1-cp312-cp312-win32.whl", hash = "sha256:5856e57e0e81f9018f1a12e803b9f768fa5533175092d72d165ac60069c7d2ab"}, - {file = "blosc2-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:585d780c5e85f251dec72b75a47666e4a261dbfe1d228769bca545e9fe07f480"}, - {file = "blosc2-2.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cb9a6ac1abc466c12bdc90052f17545512de8f854e672a1ea4d2b40292323f5"}, - {file = "blosc2-2.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3def4650faa1db43143d821228ef58797108cc95d6698c4b1581909cc2b149ca"}, - {file = "blosc2-2.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6efecc1a22da26c73ff5c60d0dc086db1e7edcceb6b360dd193cda893bef28"}, - {file = "blosc2-2.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b473472b977b770aab3bf20d0feeee84ecd5bb8b15a675287e090ce818c1cd40"}, - {file = "blosc2-2.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7afe59d35d93bf8da7db8de43f4d8aef277514de43953c1e5e416ca839b9023a"}, - {file = "blosc2-2.5.1-cp39-cp39-win32.whl", hash = "sha256:4315ae8d467fe91efa0dbe22004e967008f5fe021ebb3945518f5213d7c4511f"}, - {file = "blosc2-2.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:73eb5e569a91fbe67f7dd78efe6a1ca9a54afff2c847db5dfa675bfd6a424f60"}, - {file = "blosc2-2.5.1.tar.gz", hash = "sha256:47d5df50e7286edf81e629ece35f87f13f55c13c5e8545832188c420c75d1659"}, -] - -[package.dependencies] -msgpack = "*" -ndindex = ">=1.4" -numpy = ">=1.20.3" -py-cpuinfo = "*" - [[package]] name = "bokeh" -version = "3.4.0" +version = "3.4.1" description = "Interactive plots and applications in the browser from Python" optional = false python-versions = ">=3.9" files = [ - {file = "bokeh-3.4.0-py3-none-any.whl", hash = "sha256:d8d9ba026b734317740f90a8a58502d63c76b96c58752fc421ad4aa04df1fbcd"}, - {file = "bokeh-3.4.0.tar.gz", hash = "sha256:9ea6bc407b5e7d04ba7a2f07d8f00e8b6ffe02c2368e707f41bb362a9928569a"}, + {file = "bokeh-3.4.1-py3-none-any.whl", hash = "sha256:1e3c502a0a8205338fc74dadbfa321f8a0965441b39501e36796a47b4017b642"}, + {file = "bokeh-3.4.1.tar.gz", hash = "sha256:d824961e4265367b0750ce58b07e564ad0b83ca64b335521cd3421e9b9f10d89"}, ] [package.dependencies] @@ -420,13 +381,13 @@ xyzservices = ">=2021.09.1" [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -493,6 +454,17 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -606,6 +578,23 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-default-group" +version = "1.2.4" +description = "click_default_group" +optional = false +python-versions = ">=2.7" +files = [ + {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, + {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, +] + +[package.dependencies] +click = "*" + +[package.extras] +test = ["pytest"] + [[package]] name = "cloudpickle" version = "3.0.0" @@ -724,63 +713,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.dependencies] @@ -918,24 +907,21 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2024.4.1" +version = "2024.2.1" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2024.4.1-py3-none-any.whl", hash = "sha256:cac5d28b9de7a7cfde46d6fbd8fa81f5654980d010b44d1dbe04dd13b5b63126"}, - {file = "dask-2024.4.1.tar.gz", hash = "sha256:6cd8eb03ddc8dc08d6ca5b167b8de559872bc51cc2b6587d0e9dc754ab19cdf0"}, + {file = "dask-2024.2.1-py3-none-any.whl", hash = "sha256:a13fcdeead3bab3576495023f83097adcffe2f03c371c241b5a1f0b232b35b38"}, + {file = "dask-2024.2.1.tar.gz", hash = "sha256:9504a1e9f5d8e5403fae931f9f1660d41f510f48895ccefce856ec6a4c2198d8"}, ] [package.dependencies] click = ">=8.1" cloudpickle = ">=1.5.0" -dask-expr = {version = ">=1.0,<1.1", optional = true, markers = "extra == \"dataframe\""} fsspec = ">=2021.09.0" -importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} -numpy = {version = ">=1.21", optional = true, markers = "extra == \"array\""} +importlib-metadata = ">=4.13.0" packaging = ">=20.0" -pandas = {version = ">=1.3", optional = true, markers = "extra == \"dataframe\""} partd = ">=1.2.0" pyyaml = ">=5.3.1" toolz = ">=0.10.0" @@ -943,27 +929,11 @@ toolz = ">=0.10.0" [package.extras] array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] -dataframe = ["dask-expr (>=1.0,<1.1)", "dask[array]", "pandas (>=1.3)"] +dataframe = ["dask[array]", "pandas (>=1.3)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.4.1)"] +distributed = ["distributed (==2024.2.1)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] -[[package]] -name = "dask-expr" -version = "1.0.11" -description = "High Level Expressions for Dask" -optional = false -python-versions = ">=3.9" -files = [ - {file = "dask-expr-1.0.11.tar.gz", hash = "sha256:556e13dfa081cacb0fd14b026bde18703884c12bc4659531e440507eaaee6822"}, - {file = "dask_expr-1.0.11-py3-none-any.whl", hash = "sha256:66e1f35252875fe1875ae371bc84ab4f7f6c35692835ed545d41fac61cd0759b"}, -] - -[package.dependencies] -dask = "2024.4.1" -pandas = ">=2" -pyarrow = ">=7.0.0" - [[package]] name = "debugpy" version = "1.8.1" @@ -1017,47 +987,6 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -[[package]] -name = "diffpy-structure" -version = "3.1.0" -description = "Crystal structure container and parsers for structure formats." -optional = false -python-versions = "*" -files = [ - {file = "diffpy.structure-3.1.0.tar.gz", hash = "sha256:a8d6dee028ed864ca01a395212ace090734ee17cebf988f635333c619312db87"}, -] - -[package.dependencies] -pycifrw = ">=4.4.3" -six = "*" - -[[package]] -name = "diffsims" -version = "0.5.2" -description = "Diffraction Simulations in Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "diffsims-0.5.2-py3-none-any.whl", hash = "sha256:71db4bcb1f0c3c5d2d917b1425e5818537be5ec27c4e473cb8ace78aa75a297d"}, - {file = "diffsims-0.5.2.tar.gz", hash = "sha256:29f0facf95c88c3804479dc712b62860101b304c3e6180a74db09580129d1701"}, -] - -[package.dependencies] -"diffpy.structure" = ">=3.0.0" -matplotlib = ">=3.3" -numba = "*" -numpy = ">=1.17" -orix = ">=0.9" -psutil = "*" -scipy = ">=1.0" -tqdm = ">=4.9" -transforms3d = "*" - -[package.extras] -dev = ["black (>=19.3b0)", "coverage (>=5.0)", "furo", "manifix", "pre-commit (>=1.16)", "pytest (>=5.4)", "pytest-cov (>=2.8.1)", "pytest-xdist", "sphinx (>=3.0.2)"] -doc = ["furo", "sphinx (>=3.0.2)"] -tests = ["coverage (>=5.0)", "pytest (>=5.4)", "pytest-cov (>=2.8.1)", "pytest-xdist"] - [[package]] name = "dill" version = "0.3.8" @@ -1073,6 +1002,17 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "docutils" version = "0.20.1" @@ -1097,13 +1037,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -1137,48 +1077,6 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] -[[package]] -name = "fabio" -version = "2023.10.0" -description = "FabIO is an I/O library for images produced by 2D X-ray detectors and written in Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "fabio-2023.10.0-cp310-cp310-macosx_10_9_arm64.whl", hash = "sha256:d459ad935a7ecb52182958fcac82357c3a6a40aad99c1b18bbd2c3e32bc0e11c"}, - {file = "fabio-2023.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e9516ba9653cb9fbeee4566101c3e5319fe37a5fcc4f336ced65a49cf2031e03"}, - {file = "fabio-2023.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2882c969e3776ed39749a8f287f57750604df0f496f6b40cb6c23bab237171"}, - {file = "fabio-2023.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:4c441fdd0cc55a6dbb3e858f99ed7083a34c4e6b7ffab2cd4c4988affa07a51f"}, - {file = "fabio-2023.10.0-cp311-cp311-macosx_10_9_arm64.whl", hash = "sha256:aca8edecdde4d8ef94491aa4e54c2dcd3a56915d6828f2de42e16469347e6cdb"}, - {file = "fabio-2023.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea70ec9c2a8e969e867d24cd694c98d89195e3fef895c841b933635e15cc32a"}, - {file = "fabio-2023.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1cbe926dd5c9d8cad321876066cc5aa53a92221e2a5556e477e5243193eb39"}, - {file = "fabio-2023.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a9975f12d2b117910da8f8c1e42e4527f530b5a0045f35813e8cb2a1ebb4586e"}, - {file = "fabio-2023.10.0-cp312-cp312-macosx_10_9_arm64.whl", hash = "sha256:13e26f9daddb542316690370a5eba486267a4efb3b09c1c0cbaec650a39409e3"}, - {file = "fabio-2023.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7d58f315a957132c5477b313b48a4e0a0f7c89139259f38bb7c990486ba4676"}, - {file = "fabio-2023.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d6c8784fa3677a0634306fb400edef01f6f15222b6eb843c2b054838bd268839"}, - {file = "fabio-2023.10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f6ca9c22c27bc0940b825c34d4e73f067c914b4257f19cb6f0ba7a9599625e9e"}, - {file = "fabio-2023.10.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec0ad50b7606a1b81b8b66ed7e9d2d4cadb93d9337409cd548678b6c7f3d936"}, - {file = "fabio-2023.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b44c94a0c8eee759b35900b027d9a408104d71fa60eba2bb7a9c83d5ce351da"}, - {file = "fabio-2023.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:07ec0ae4780d2ddad767b74dc9c2da189675fa5d9011c61f6182b909e628392d"}, - {file = "fabio-2023.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20de716de69fedfbe02f9595a02bfdd11a35bc13dd18deacd086515a2d295c55"}, - {file = "fabio-2023.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7cc22953cb7e43a3898d13eb15bd21b3ccc74f98f6010430d46e03e4f4b8bb15"}, - {file = "fabio-2023.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f642f2ad47a5f9659db91baec45fc7fc22afa46770e17928a2620737d5e202f"}, - {file = "fabio-2023.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:616484d685123b07025bf9afb072d3b652b8f077c7d983144befb896925ccf70"}, - {file = "fabio-2023.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:de3ac5702866a305f26c4228c590d3c10d061979d5d09575b76eae62cdc178a4"}, - {file = "fabio-2023.10.0-cp39-cp39-macosx_10_9_arm64.whl", hash = "sha256:5996373168576d89ad3a0af542c5c8113675103219389911f6198013240f3e07"}, - {file = "fabio-2023.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a30493b5ae8b19dfbd5c2a7f9f190a13b6f2e7e5f6600070ce5be9c32fdc1675"}, - {file = "fabio-2023.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47fb1dc59c57dbccaaf32c1b0b2b2ff4f9e7d2c37b2ea994b09cd3f65dc66903"}, - {file = "fabio-2023.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:526aace00f45d4eeff805d349fe5b9ca8c89f6942719ebb86245870883390e0b"}, - {file = "fabio-2023.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:ebf02230fbb2cc8e9965d9cbdb88eef8fa747547727612df5e11162deb3cd168"}, - {file = "fabio-2023.10.0.tar.gz", hash = "sha256:fc3edcf03f833a11e09f987cffc4b825b5964f57e2866e28f9edf5d67532c405"}, -] - -[package.dependencies] -h5py = "*" -hdf5plugin = "*" -lxml = "*" -numpy = "*" -pillow = "*" - [[package]] name = "fastdtw" version = "0.3.4" @@ -1193,26 +1091,15 @@ files = [ [package.dependencies] numpy = "*" -[[package]] -name = "fasteners" -version = "0.19" -description = "A python package that provides useful locks" -optional = false -python-versions = ">=3.6" -files = [ - {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, - {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, -] - [[package]] name = "fastjsonschema" -version = "2.19.1" +version = "2.20.0" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" files = [ - {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"}, - {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"}, + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, ] [package.extras] @@ -1220,56 +1107,49 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fastparquet" -version = "2024.2.0" +version = "2024.5.0" description = "Python support for Parquet file format" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "fastparquet-2024.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:94aaa752d79660f2d88983bd7336109f4b61da6940d759786c02144195d6c635"}, - {file = "fastparquet-2024.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abb08c61ab0f8a29a118dabe0a9105686fa5580648cfca252a74153c8c32444f"}, - {file = "fastparquet-2024.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d04901828f54ec118e7e5dfb438518ffe9b75ef3b7ebcdbaf33af130fcee9b7"}, - {file = "fastparquet-2024.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42def5e682eb426e6f7062d0bee370dec9424181f3c61eb24d6bdc67482a0ace"}, - {file = "fastparquet-2024.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d87f24ae76e65f94af9e62a648b5479f0bd2e8935e0011c9390ebc1299f3785d"}, - {file = "fastparquet-2024.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:76fadf2399a778daf49772c644a3a7b27e41492a43e2bea4107a715981c1dc2f"}, - {file = "fastparquet-2024.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83f1abb155d8a8b6f1f31318174507d8a8ddf4bff00a2ef7065b609577deb6ae"}, - {file = "fastparquet-2024.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:dedeb4ad28f68313c2504ef005f4b2d52c3d108bd5323204300dbaeec6fb1b04"}, - {file = "fastparquet-2024.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b7c39661c918686fdbf21695547d2e7b0cd0226a2f2dd6fa5c2ad7b37da2540"}, - {file = "fastparquet-2024.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd1b310e7d9934f61236b793d1e11336d457e7664829bf76d53bff5614dcc338"}, - {file = "fastparquet-2024.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e27b5d21fecdc07f071f5343a350b88c859b324834fd19b78d636480fe341999"}, - {file = "fastparquet-2024.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e3c5cdf2af0fc1b76f07daabd37b132c0f0086106b2fc801ea046739ddabee0"}, - {file = "fastparquet-2024.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea1503bac0b1457c016a748064823d312806e506f3a8b9226935def4be3fffdc"}, - {file = "fastparquet-2024.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b76febb17f2261e1aa8bdf11b3459ee9cca19ced25744b940c3922b7d93862f9"}, - {file = "fastparquet-2024.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a14579bbe2fab4f5f43685503b4142d8b0eb7965ee176704ae1697590143cd1"}, - {file = "fastparquet-2024.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0c1edc578f7a9919d1062bc3184c0c64d5c4e986ab3fa9c75f53561bb7364d7f"}, - {file = "fastparquet-2024.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:cebc1adc7c3a1aed70c752f3fde5e4df094dafba24e60d6501d7963e77047e7e"}, - {file = "fastparquet-2024.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c26266910e42190f3ba043647b4c1e37e8626981a0366432a498bdf1e10c0bd1"}, - {file = "fastparquet-2024.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee37d9273e383811f10bd379990851b53df606cfaa046cae53826b6b14f0a33d"}, - {file = "fastparquet-2024.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42babeafac01ab24ea1edc7f626c0744c312d60ba6a7189b08c8e7d1c374bfd3"}, - {file = "fastparquet-2024.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b7a620b87e83c098a46611b901c456403c9a04ba526e4a615750d6704092e1eb"}, - {file = "fastparquet-2024.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:e6f544d65b9f826a149010e3fd5121510e0a1a44c62f1b274aea4a41a8f3dbcd"}, - {file = "fastparquet-2024.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf6df4a9c781e32dc10432e78ee82c3c8750e9975a4e2d29aecffc1f2323a418"}, - {file = "fastparquet-2024.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ee36f1ea8f08cb9b8710161eee4e752e74f34ef3e7aebc58db4e5468d29ff34c"}, - {file = "fastparquet-2024.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd4b8133f5fa43c497d151d4d00337f9b0614993116a61c61e563a003eb0811e"}, - {file = "fastparquet-2024.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6509837887e35bdcb08ba252eeb930b1056e129b6d31c14901443339567ee95a"}, - {file = "fastparquet-2024.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f369dcc860b176739826ed67ea230f243334df5c5b3047ac10b0a365ec469082"}, - {file = "fastparquet-2024.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fe1b88f51687566eac9fa94f7ce4f17b8df9e4b7ba8f7d37f383e7140414fe98"}, - {file = "fastparquet-2024.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d2711f30720c4f80654c191ecb21d2b1b7351be1f6763c70936bdbab095f0b54"}, - {file = "fastparquet-2024.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:52603d24d19522753e21b1794d99bb295688e33d1a04b61a5c0e9eb4884ba342"}, - {file = "fastparquet-2024.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6affd18ed2608976739b47befce9f80f7848209c892ccb1001d494296af33af"}, - {file = "fastparquet-2024.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a7314e654a06cfc68a50bfc61bbacc548257d8742fbecfe0418c3b0d4295c04"}, - {file = "fastparquet-2024.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba0fcba4ffd60ab23d24486f85733a5cc1fcf46d1286c9dc3eed329809e9ee3"}, - {file = "fastparquet-2024.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dace50138c81c6f70acfff91a7a15acc85e3d45be0edbcf164f26fd86cf3c7a5"}, - {file = "fastparquet-2024.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd45a7973afe651d7fdb6b836fa1f9177d318de20211a28f4580d9af5c2aacbb"}, - {file = "fastparquet-2024.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33121c1596bb4d672579969a4901730f555447204c7c2573621803f7990cd309"}, - {file = "fastparquet-2024.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5131d77a6c4cdfe3b00baa7eb95602c7f09d955c5490dd3bc0ec0e290ee4010"}, - {file = "fastparquet-2024.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:06736e5bb0827f861ac0901310baedf7e7b5f52dfcd89d435963ae328203597c"}, - {file = "fastparquet-2024.2.0.tar.gz", hash = "sha256:81a8f60c51793eb2436b4fdbbf115ff8578a4a457a179240bc08f9d9573d57a4"}, + {file = "fastparquet-2024.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9dfbed87b4b58b0794b2cb3aa4abcb43fc01480a10c7779a323d2dd1599f6acd"}, + {file = "fastparquet-2024.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07fc5a45450a39cd07c6ef0e0219ac4b1879f8b27c825ee4ba5d87a3ae505f11"}, + {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2045c21f90358541286f26f0735bfb2265b075413fbced3b876fc8848eda52"}, + {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f411056152b5d3cc82b6624d9da80535d10d9277d921fdb2e9516e93c8c227e8"}, + {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc99d7c0f1816394d53aadd47919bba70bb81355259d8788d28e35913816aee0"}, + {file = "fastparquet-2024.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:42149929b71d9122bd501aa695681f40a04a9fa3f5b802cf0fb6aa4e95ccf2dd"}, + {file = "fastparquet-2024.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5b1ed889f4ac7ea059ff95f4a01f5c07c825c50c2e1bc9e2b64c814df94c243"}, + {file = "fastparquet-2024.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:f5c3cabcfa2f534e4b23343c1ab84c37d336da73770005e608d1894ab1084600"}, + {file = "fastparquet-2024.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56d03b0a291d6a575ab365516c53b4da8e040347f8d43af79be25893c591b38c"}, + {file = "fastparquet-2024.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:784989ee2c251960b8f00dc38c6c730f784712c8e3d08cc7e0ce842055476af1"}, + {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d20bba5c39139a88d8d6931764b830ba14042742d802238d9edf86d4d765ad7a"}, + {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08358d99278c5d3fb523d819fff5c74d572d8f67ebbe2215a2c7bfca7e3664cf"}, + {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9de270e17a6ae2f02c716421d60e18d35d4718037f561b3e359989db19f700a"}, + {file = "fastparquet-2024.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba251231b005c0f3f7e56f6e9cd1939be99b2d810ab5b05039271e260c0196c6"}, + {file = "fastparquet-2024.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1496d83d7a77c19abae796e3b582539884fc893d75a3ad4f90df12f8f23a902a"}, + {file = "fastparquet-2024.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea3796c4a38ef8b372a3056b5cef52ca8182fa554fa51c7637c2421e69ee56e5"}, + {file = "fastparquet-2024.5.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1fa068ef1826bff6d4a9106a6f9e9d6fd20b8b516da4b82d87840cb5fd3947c"}, + {file = "fastparquet-2024.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a60f7b0b308d6b9f12c642cf5237a05d754926fb31ce865ff7072bceab19fbb"}, + {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6ac308a2f391ce589c99b8376e7cdfe4241ef5770ac4cf4c1c93f940bda83c"}, + {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b3cf7b4eb1b06e87b97a3a5c9124e4b1c08a8903ba017052c5fe2c482414a3d"}, + {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5626fc72204001b7e82fedb4b02174ecb4e2d4143b38b4ea8d2f9eb65f6b000e"}, + {file = "fastparquet-2024.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c8b2e86fe6488cce0e3d41263bb0296ef9bbb875a2fca09d67d7685640017a66"}, + {file = "fastparquet-2024.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2a951106782d51e5ab110beaad29c4aa0537f045711bb0bf146f65aeaed14174"}, + {file = "fastparquet-2024.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:47695037fdc534ef4247f25ccf17dcbd8825be6ecb70c54ca54d588a794f4a6d"}, + {file = "fastparquet-2024.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc3d35ff8341cd65baecac71062e9d73393d7afda207b3421709c1d3f4baa194"}, + {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:691348cc85890663dd3c0bb02544d38d4c07a0c3d68837324dc01007301150b5"}, + {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfdc8aaec67edd30814c2c2f0e291eb3c3044525d18c87e835ef8793d6e2ea2d"}, + {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0034d1b5af3a71cc2fb29c590f442c0b514f710d6d6996794ae375dcfe050c05"}, + {file = "fastparquet-2024.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b562be0f43a007493014512602ab6b0207d13ea4ae85e0d94d61febf08efa1ee"}, + {file = "fastparquet-2024.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:611da9043f9dab1c63e6c90a6b124e3d2789c34fefa00d45356517f1e8a09c83"}, + {file = "fastparquet-2024.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb93e8951f46943c8567c9a555cb3d24d2c78efdf78e95fd72177d80da73a10f"}, + {file = "fastparquet-2024.5.0.tar.gz", hash = "sha256:dffd1d0ac6e89e31c5b6dacf67a8d299d4afbbcf0bf8b797373904c819c48f51"}, ] [package.dependencies] cramjam = ">=2.3" fsspec = "*" -numpy = ">=1.20.3" +numpy = "*" packaging = "*" pandas = ">=1.5.0" @@ -1277,64 +1157,70 @@ pandas = ">=1.5.0" lzo = ["python-lzo"] [[package]] -name = "flatdict" -version = "4.0.1" -description = "Python module for interacting with nested dicts as a single level dict with delimited keys." +name = "filelock" +version = "3.15.3" +description = "A platform independent file lock." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "flatdict-4.0.1.tar.gz", hash = "sha256:cd32f08fd31ed21eb09ebc76f06b6bd12046a24f77beb1fd0281917e47f26742"}, + {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, + {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, ] +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "fonttools" -version = "4.51.0" +version = "4.53.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, - {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, - {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, - {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, - {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, - {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, - {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, - {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, - {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, - {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, - {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, - {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, - {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, + {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, + {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, + {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, + {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, + {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, + {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, + {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, + {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, + {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, + {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, + {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, + {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, + {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, + {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, ] [package.extras] @@ -1364,13 +1250,13 @@ files = [ [[package]] name = "fsspec" -version = "2024.3.1" +version = "2024.6.0" description = "File-system specification" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, - {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, + {file = "fsspec-2024.6.0-py3-none-any.whl", hash = "sha256:58d7122eb8a1a46f7f13453187bfea4972d66bf01618d37366521b1998034cee"}, + {file = "fsspec-2024.6.0.tar.gz", hash = "sha256:f579960a56e6d8038a9efc8f9c77279ec12e6299aa86b0769a7e9c46b94527c2"}, ] [package.extras] @@ -1378,7 +1264,8 @@ abfs = ["adlfs"] adl = ["adlfs"] arrow = ["pyarrow (>=1)"] dask = ["dask", "distributed"] -devel = ["pytest", "pytest-cov"] +dev = ["pre-commit", "ruff"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] dropbox = ["dropbox", "dropboxdrivefs", "requests"] full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] fuse = ["fusepy"] @@ -1395,51 +1282,11 @@ s3 = ["s3fs"] sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] -[[package]] -name = "future" -version = "1.0.0" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, - {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, -] - -[[package]] -name = "gitdb" -version = "4.0.11" -description = "Git Object Database" -optional = false -python-versions = ">=3.7" -files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, -] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.43" -description = "GitPython is a Python library used to interact with Git repositories" -optional = false -python-versions = ">=3.7" -files = [ - {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, - {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, -] - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[package.extras] -doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] - [[package]] name = "h5grove" version = "1.3.0" @@ -1519,71 +1366,18 @@ dev = ["sphinx", "sphinx-rtd-theme"] test = ["blosc2 (>=2.5.1)", "blosc2-grok (>=0.2.2)"] [[package]] -name = "hyperspy" -version = "1.7.4" -description = "Multidimensional data analysis toolbox" +name = "identify" +version = "2.5.36" +description = "File identification library for Python" optional = false -python-versions = "~=3.6" +python-versions = ">=3.8" files = [ - {file = "hyperspy-1.7.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:99ae5af1aede89c29298fa097382b3a7ccf60392aae69773d1c5421de7274474"}, - {file = "hyperspy-1.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:98d3e611dae0c66f33dada0b6751744ebf7f753e0b1f4a10e87bee02695f3208"}, - {file = "hyperspy-1.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:7b53baf37f694bcea415a535d0c82c8953a338ce21891da0dfdb7a2747017140"}, - {file = "hyperspy-1.7.4-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:c59762cf51325c667480b9f17a20c392baa45d0895b054343343cadb6ee300b6"}, - {file = "hyperspy-1.7.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:987f98f076d71283bf4cc81f622616b80eb10f0cd3e7df9a4301142946aeb4bd"}, - {file = "hyperspy-1.7.4-cp37-cp37m-win_amd64.whl", hash = "sha256:39749c7cbffa1bf16ed99399648951d49aca4bfe42f1ff20f67d2cf042f864c5"}, - {file = "hyperspy-1.7.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:aacb3014ae4bd596f811392bc637cbc6d5ac497af9137166e9e152a93316249f"}, - {file = "hyperspy-1.7.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:27bf19820c267df980e92c07f129a6065bf34fe548dc2d673a4cfae80e5742d4"}, - {file = "hyperspy-1.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:6158673c2c2d0b5b64dead0f414a3062eda58be22dff6d1bb513b5c20bd7236a"}, - {file = "hyperspy-1.7.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:8e1a5a7e2961fda3464edb8b4e6d376cf8bf28c7b2c1d15092d0693f87413d94"}, - {file = "hyperspy-1.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c0874f5d4f3f043b92b198f1b66fd3505bf35e9bbbd74bff191de3ed6e721795"}, - {file = "hyperspy-1.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:e49c35e1bd1d5bb24b812226fcda41083a0cbf522910e894780cd9eaad92afcf"}, - {file = "hyperspy-1.7.4.tar.gz", hash = "sha256:fb5dda43f4af612831799c89b0a953147f6325a064d06144105e6911a30baa5d"}, + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] -[package.dependencies] -dask = {version = ">=2.11.0", extras = ["array"]} -dill = "*" -fsspec = "*" -h5py = ">=2.3" -imageio = "*" -importlib-metadata = ">=3.6" -ipyparallel = "*" -ipython = "!=8.0.*" -jinja2 = "*" -matplotlib = ">=3.1.3" -natsort = "*" -numba = ">=0.52" -numexpr = "*" -numpy = ">=1.17.1" -packaging = "*" -pint = ">=0.10" -prettytable = "*" -python-dateutil = ">=2.5.0" -pyyaml = "*" -requests = "*" -scikit-image = ">=0.15" -scipy = ">=1.4.0" -sparse = "*" -sympy = "*" -tifffile = ">=2020.2.16" -toolz = "*" -tqdm = ">=4.9.0" -traits = ">=4.5.0" -zarr = {version = ">=2.9.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"AMD64\""} - [package.extras] -all = ["blosc (>=1.5)", "cython", "hyperspy-gui-ipywidgets (>=1.1.0)", "hyperspy-gui-traitsui (>=1.1.0)", "imagecodecs (>=2020.1.31)", "matplotlib-scalebar", "mrcz (>=0.3.6)", "pyUSID (>=0.0.7)", "scikit-learn (>=1.0.1)", "sidpy"] -build-doc = ["sphinx (>=1.7)", "sphinx-rtd-theme", "sphinx-toggleprompt", "sphinxcontrib-mermaid", "sphinxcontrib-towncrier", "towncrier (<22.8)"] -coverage = ["pytest-cov"] -dev = ["blosc (>=1.5)", "cython", "hyperspy-gui-ipywidgets (>=1.1.0)", "hyperspy-gui-traitsui (>=1.1.0)", "imagecodecs (>=2020.1.31)", "matplotlib (>=3.1)", "matplotlib-scalebar", "mrcz (>=0.3.6)", "pooch", "pyUSID (>=0.0.7)", "pytest (>=3.6)", "pytest-cov", "pytest-instafail", "pytest-mpl", "pytest-rerunfailures", "pytest-xdist", "scikit-learn (>=1.0.1)", "sidpy", "sphinx (>=1.7)", "sphinx-rtd-theme", "sphinx-toggleprompt", "sphinxcontrib-mermaid", "sphinxcontrib-towncrier", "towncrier (<22.8)"] -gui-jupyter = ["hyperspy-gui-ipywidgets (>=1.1.0)"] -gui-traitsui = ["hyperspy-gui-traitsui (>=1.1.0)"] -learning = ["scikit-learn (>=1.0.1)"] -mrcz = ["blosc (>=1.5)", "mrcz (>=0.3.6)"] -scalebar = ["matplotlib-scalebar"] -speed = ["cython", "imagecodecs (>=2020.1.31)"] -tests = ["matplotlib (>=3.1)", "pooch", "pytest (>=3.6)", "pytest-instafail", "pytest-mpl", "pytest-rerunfailures", "pytest-xdist"] -usid = ["pyUSID (>=0.0.7)", "sidpy"] +license = ["ukkonen"] [[package]] name = "idna" @@ -1596,37 +1390,15 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "ifes-apt-tc-data-modeling" -version = "0.1" -description = "Foster exchange about data models and work towards clear specifications of file formats and data models in the research fields of atom probe tomography and related field-ion microscopy (atom probe microscopy)." -optional = false -python-versions = ">=3.8" -files = [ - {file = "ifes_apt_tc_data_modeling-0.1-py3-none-any.whl", hash = "sha256:02f0c49f92bd30cc5ef5a4995f2cda7506cb11bd14fd202b765b3acf2aee9cea"}, - {file = "ifes_apt_tc_data_modeling-0.1.tar.gz", hash = "sha256:6a82215279cbf2233f6a37b94f7d06c1314fb23aa865e2d499fc9a076740e56e"}, -] - -[package.dependencies] -ase = ">=3.19.0" -h5py = ">=3.6.0" -numpy = ">=1.21.2" -pandas = "*" -radioactivedecay = ">=0.4.16" -tables = "*" - -[package.extras] -dev = ["jupyterlab", "jupyterlab-h5web", "twine"] - [[package]] name = "imageio" -version = "2.34.0" +version = "2.34.1" description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." optional = false python-versions = ">=3.8" files = [ - {file = "imageio-2.34.0-py3-none-any.whl", hash = "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba"}, - {file = "imageio-2.34.0.tar.gz", hash = "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53"}, + {file = "imageio-2.34.1-py3-none-any.whl", hash = "sha256:408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c"}, + {file = "imageio-2.34.1.tar.gz", hash = "sha256:f13eb76e4922f936ac4a7fec77ce8a783e63b93543d4ea3e40793a6cabd9ac7d"}, ] [package.dependencies] @@ -1663,22 +1435,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "7.2.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, + {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" @@ -1744,13 +1516,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipympl" -version = "0.9.3" +version = "0.9.4" description = "Matplotlib Jupyter Extension" optional = false -python-versions = "*" +python-versions = ">=3.9" files = [ - {file = "ipympl-0.9.3-py2.py3-none-any.whl", hash = "sha256:d113cd55891bafe9b27ef99b6dd111a87beb6bb2ae550c404292272103be8013"}, - {file = "ipympl-0.9.3.tar.gz", hash = "sha256:49bab75c05673a6881d1aaec5d8ac81d4624f73d292d154c5fb7096f10236a2b"}, + {file = "ipympl-0.9.4-py3-none-any.whl", hash = "sha256:5b0c08c6f4f6ea655ba58239363457c10fb921557f5038c1a46db4457d6d6b0e"}, + {file = "ipympl-0.9.4.tar.gz", hash = "sha256:cfb53c5b4fcbcee6d18f095eecfc6c6c474303d5b744e72cc66e7a2804708907"}, ] [package.dependencies] @@ -1763,39 +1535,7 @@ pillow = "*" traitlets = "<6" [package.extras] -docs = ["Sphinx (>=1.5)", "myst-nb", "sphinx-book-theme", "sphinx-copybutton", "sphinx-thebe", "sphinx-togglebutton"] - -[[package]] -name = "ipyparallel" -version = "8.8.0" -description = "Interactive Parallel Computing with IPython" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ipyparallel-8.8.0-py3-none-any.whl", hash = "sha256:d2db60fd8baea09e02e7a13a761e1798868c0a717e3e2fa8fb52ffc037cccd35"}, - {file = "ipyparallel-8.8.0.tar.gz", hash = "sha256:2404d59f86a3aaa3bd27bf6b57df777bff5c1363c1c6e60403759d16ed42dc7b"}, -] - -[package.dependencies] -decorator = "*" -entrypoints = "*" -ipykernel = ">=4.4" -ipython = ">=4" -jupyter-client = ">=5" -psutil = "*" -python-dateutil = ">=2.1" -pyzmq = ">=18" -tornado = ">=5.1" -tqdm = "*" -traitlets = ">=4.3" - -[package.extras] -benchmark = ["asv"] -labextension = ["jupyter-server", "jupyterlab (>=3)"] -nbext = ["jupyter-server", "notebook"] -retroextension = ["jupyter-server", "retrolab"] -serverextension = ["jupyter-server"] -test = ["ipython[test]", "pytest", "pytest-asyncio", "pytest-cov", "testpath"] +docs = ["myst-nb", "sphinx (>=1.5)", "sphinx-book-theme", "sphinx-copybutton", "sphinx-thebe", "sphinx-togglebutton"] [[package]] name = "ipython" @@ -1902,13 +1642,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1919,46 +1659,46 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.4.0" +version = "1.4.2" description = "Lightweight pipelining with Python functions" optional = false python-versions = ">=3.8" files = [ - {file = "joblib-1.4.0-py3-none-any.whl", hash = "sha256:42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7"}, - {file = "joblib-1.4.0.tar.gz", hash = "sha256:1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c"}, + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, ] [[package]] name = "json5" -version = "0.9.24" +version = "0.9.25" description = "A Python implementation of the JSON5 data format." optional = true python-versions = ">=3.8" files = [ - {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"}, - {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"}, + {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, + {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, ] [[package]] name = "jsonpointer" -version = "2.4" +version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +python-versions = ">=3.7" files = [ - {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, - {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, ] [[package]] name = "jsonschema" -version = "4.21.1" +version = "4.22.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, - {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, + {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, + {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, ] [package.dependencies] @@ -2140,13 +1880,13 @@ test = ["coverage", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console [[package]] name = "jupyter-server-fileid" -version = "0.9.1" +version = "0.9.2" description = "Jupyter Server extension providing an implementation of the File ID service." optional = true python-versions = ">=3.7" files = [ - {file = "jupyter_server_fileid-0.9.1-py3-none-any.whl", hash = "sha256:76dd05a45b78c7ec0cba0be98ece289984c6bcfc1ca2da216d42930e506a4d68"}, - {file = "jupyter_server_fileid-0.9.1.tar.gz", hash = "sha256:7486bca3acf9bbaab7ce5127f9f64d2df58f5d2de377609fb833291a7217a6a2"}, + {file = "jupyter_server_fileid-0.9.2-py3-none-any.whl", hash = "sha256:76a2fbcea6950968485dcd509c2d6ac417ca11e61ab1ad447a475f0878ca808f"}, + {file = "jupyter_server_fileid-0.9.2.tar.gz", hash = "sha256:ffb11460ca5f8567644f6120b25613fca8e3f3048b38d14c6e3fe1902f314a9b"}, ] [package.dependencies] @@ -2226,13 +1966,13 @@ test = ["check-manifest", "coverage", "jupyterlab-server[test]", "pre-commit", " [[package]] name = "jupyterlab-h5web" -version = "8.0.0" +version = "8.1.0" description = "A JupyterLab extension to explore and visualize HDF5 file contents." optional = false python-versions = ">=3.7" files = [ - {file = "jupyterlab_h5web-8.0.0-py3-none-any.whl", hash = "sha256:096e4dafaa2871e9bddc180c2a6c1e760a99e2de99352320841a5c7645fb36a9"}, - {file = "jupyterlab_h5web-8.0.0.tar.gz", hash = "sha256:a713ff7e10ce69f9f51bc158330829f2196220e2755ae8a720def37ce1db4841"}, + {file = "jupyterlab_h5web-8.1.0-py3-none-any.whl", hash = "sha256:99c6136500f317f2fbecce97be4b3515d6cf25410fc9274e3243169385a85dfd"}, + {file = "jupyterlab_h5web-8.1.0.tar.gz", hash = "sha256:784c6cafff088725cbc73fef8d6181bb690a1fc414868f74be9a3f57557b16b6"}, ] [package.dependencies] @@ -2257,13 +1997,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.26.0" +version = "2.27.2" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = true python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.26.0-py3-none-any.whl", hash = "sha256:54622cbd330526a385ee0c1fdccdff3a1e7219bf3e864a335284a1270a1973df"}, - {file = "jupyterlab_server-2.26.0.tar.gz", hash = "sha256:9b3ba91cf2837f7f124fca36d63f3ca80ace2bed4898a63dd47e6598c1ab006f"}, + {file = "jupyterlab_server-2.27.2-py3-none-any.whl", hash = "sha256:54aa2d64fd86383b5438d9f0c032f043c4d8c0264b8af9f60bd061157466ea43"}, + {file = "jupyterlab_server-2.27.2.tar.gz", hash = "sha256:15cbb349dc45e954e09bacf81b9f9bcb10815ff660fb2034ecd7417db3a7ea27"}, ] [package.dependencies] @@ -2292,41 +2032,6 @@ files = [ {file = "jupyterlab_widgets-1.1.7.tar.gz", hash = "sha256:318dab34267915d658e7b0dc57433ff0ce0d52b3e283986b73b66f7ab9017ae8"}, ] -[[package]] -name = "kikuchipy" -version = "0.9.0" -description = "Processing, simulating and indexing of electron backscatter diffraction (EBSD) patterns." -optional = false -python-versions = ">=3.7" -files = [ - {file = "kikuchipy-0.9.0-py3-none-any.whl", hash = "sha256:7f9e2c8c6d71f8382a3724d14ec2a5dd41a47a1f3e6aeee532498bf98c915a87"}, - {file = "kikuchipy-0.9.0.tar.gz", hash = "sha256:f0523ac0245d59c598fbaa0c62da46e5b08de306a62a7a99d4c140cc422772be"}, -] - -[package.dependencies] -dask = {version = ">=2021.8.1", extras = ["array"]} -"diffpy.structure" = ">=3" -diffsims = ">=0.5.1" -h5py = ">=2.10" -hyperspy = ">=1.7.3,<2" -imageio = "*" -matplotlib = ">=3.5" -numba = ">=0.55" -numpy = ">=1.21.6" -orix = ">=0.11.1" -pooch = ">=1.3.0" -pyyaml = "*" -scikit-image = ">=0.16.2" -scikit-learn = "*" -scipy = ">=1.7" -tqdm = ">=0.5.2" - -[package.extras] -all = ["matplotlib (>=3.5)", "nlopt", "pyebsdindex (>=0.2,<1.0)", "pyvista"] -dev = ["black[jupyter] (>=23.1)", "coverage (>=5.0)", "manifix", "matplotlib (>=3.5)", "memory-profiler", "nbsphinx (>=0.7)", "nlopt", "numpydoc", "outdated", "pre-commit (>=1.16)", "pydata-sphinx-theme", "pyebsdindex (>=0.2,<1.0)", "pytest (>=5.4)", "pytest-benchmark", "pytest-cov (>=2.8.1)", "pytest-rerunfailures", "pytest-xdist", "pyvista", "sphinx (>=3.0.2)", "sphinx-codeautolink[ipython] (<0.14)", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery", "sphinxcontrib-bibtex (>=1.0)"] -doc = ["memory-profiler", "nbsphinx (>=0.7)", "nlopt", "numpydoc", "pydata-sphinx-theme", "pyebsdindex (>=0.2,<1.0)", "pyvista", "sphinx (>=3.0.2)", "sphinx-codeautolink[ipython] (<0.14)", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery", "sphinxcontrib-bibtex (>=1.0)"] -tests = ["coverage (>=5.0)", "numpydoc", "pytest (>=5.4)", "pytest-benchmark", "pytest-cov (>=2.8.1)", "pytest-rerunfailures", "pytest-xdist"] - [[package]] name = "kiwisolver" version = "1.4.5" @@ -2461,43 +2166,43 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "llvmlite" -version = "0.42.0" +version = "0.43.0" description = "lightweight wrapper around basic LLVM functionality" optional = false python-versions = ">=3.9" files = [ - {file = "llvmlite-0.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3366938e1bf63d26c34fbfb4c8e8d2ded57d11e0567d5bb243d89aab1eb56098"}, - {file = "llvmlite-0.42.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c35da49666a21185d21b551fc3caf46a935d54d66969d32d72af109b5e7d2b6f"}, - {file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f44ccc3c6220bd23e0ba698a63ec2a7d3205da0d848804807f37fc243e3f77"}, - {file = "llvmlite-0.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763f8d8717a9073b9e0246998de89929071d15b47f254c10eef2310b9aac033d"}, - {file = "llvmlite-0.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:8d90edf400b4ceb3a0e776b6c6e4656d05c7187c439587e06f86afceb66d2be5"}, - {file = "llvmlite-0.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae511caed28beaf1252dbaf5f40e663f533b79ceb408c874c01754cafabb9cbf"}, - {file = "llvmlite-0.42.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81e674c2fe85576e6c4474e8c7e7aba7901ac0196e864fe7985492b737dbab65"}, - {file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3975787f13eb97629052edb5017f6c170eebc1c14a0433e8089e5db43bcce6"}, - {file = "llvmlite-0.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5bece0cdf77f22379f19b1959ccd7aee518afa4afbd3656c6365865f84903f9"}, - {file = "llvmlite-0.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e0c4c11c8c2aa9b0701f91b799cb9134a6a6de51444eff5a9087fc7c1384275"}, - {file = "llvmlite-0.42.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:08fa9ab02b0d0179c688a4216b8939138266519aaa0aa94f1195a8542faedb56"}, - {file = "llvmlite-0.42.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2fce7d355068494d1e42202c7aff25d50c462584233013eb4470c33b995e3ee"}, - {file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe66a86dc44634b59a3bc860c7b20d26d9aaffcd30364ebe8ba79161a9121f4"}, - {file = "llvmlite-0.42.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47494552559e00d81bfb836cf1c4d5a5062e54102cc5767d5aa1e77ccd2505c"}, - {file = "llvmlite-0.42.0-cp312-cp312-win_amd64.whl", hash = "sha256:05cb7e9b6ce69165ce4d1b994fbdedca0c62492e537b0cc86141b6e2c78d5888"}, - {file = "llvmlite-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bdd3888544538a94d7ec99e7c62a0cdd8833609c85f0c23fcb6c5c591aec60ad"}, - {file = "llvmlite-0.42.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0936c2067a67fb8816c908d5457d63eba3e2b17e515c5fe00e5ee2bace06040"}, - {file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a78ab89f1924fc11482209f6799a7a3fc74ddc80425a7a3e0e8174af0e9e2301"}, - {file = "llvmlite-0.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7599b65c7af7abbc978dbf345712c60fd596aa5670496561cc10e8a71cebfb2"}, - {file = "llvmlite-0.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:43d65cc4e206c2e902c1004dd5418417c4efa6c1d04df05c6c5675a27e8ca90e"}, - {file = "llvmlite-0.42.0.tar.gz", hash = "sha256:f92b09243c0cc3f457da8b983f67bd8e1295d0f5b3746c7a1861d7a99403854a"}, + {file = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"}, + {file = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"}, + {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead"}, + {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a"}, + {file = "llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed"}, + {file = "llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98"}, + {file = "llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57"}, + {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2"}, + {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749"}, + {file = "llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91"}, + {file = "llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7"}, + {file = "llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7"}, + {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f"}, + {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844"}, + {file = "llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9"}, + {file = "llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c"}, + {file = "llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8"}, + {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a"}, + {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867"}, + {file = "llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4"}, + {file = "llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5"}, ] [[package]] name = "lmfit" -version = "1.3.0" +version = "1.3.1" description = "Least-Squares Minimization with Bounds and Constraints" optional = false python-versions = ">=3.8" files = [ - {file = "lmfit-1.3.0-py3-none-any.whl", hash = "sha256:84359d930c659e9b0e49d8a4ea4d81ba24b252c3b29afca03888be54cbda12da"}, - {file = "lmfit-1.3.0.tar.gz", hash = "sha256:7e823aebc05237ca80d33d260cedbba5e26190ccec9faf595e68143909de0c93"}, + {file = "lmfit-1.3.1-py3-none-any.whl", hash = "sha256:5486c19fe6b3899e4562b9976dee42d1a064a81ce76fe280a9970398fa300312"}, + {file = "lmfit-1.3.1.tar.gz", hash = "sha256:bc386244adbd10ef1a2a2c4f9d17b3def905552c0a64aef854f0ad46cc309cc5"}, ] [package.dependencies] @@ -2510,7 +2215,7 @@ uncertainties = ">=3.1.4" [package.extras] all = ["lmfit[dev,doc,test]"] dev = ["build", "check-wheel-contents", "flake8-pyproject", "pre-commit", "twine"] -doc = ["Pillow", "Sphinx", "cairosvg", "corner", "emcee (>=3.0.0)", "ipykernel", "jupyter-sphinx (>=0.2.4)", "matplotlib", "numdifftools", "pandas", "pycairo", "sphinx-gallery (>=0.10)", "sphinxcontrib-svg2pdfconverter", "sympy"] +doc = ["Pillow", "Sphinx", "cairosvg", "corner", "emcee (>=3.0.0)", "ipykernel", "jupyter-sphinx (>=0.2.4)", "matplotlib", "numdifftools", "numexpr", "pandas", "pycairo", "sphinx-gallery (>=0.10)", "sphinxcontrib-svg2pdfconverter", "sympy"] test = ["coverage", "flaky", "pytest", "pytest-cov"] [[package]] @@ -2526,166 +2231,153 @@ files = [ [[package]] name = "lxml" -version = "5.2.1" +version = "5.2.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, - {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, - {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, - {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, - {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, - {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, - {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, - {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, - {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, - {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, - {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, - {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, - {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, - {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, - {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, - {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, - {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"}, + {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"}, + {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, + {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, + {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, + {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, + {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, + {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"}, + {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"}, + {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, + {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, + {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, + {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, + {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"}, + {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"}, + {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"}, + {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"}, + {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, + {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, ] [package.extras] @@ -2790,39 +2482,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.8.4" +version = "3.9.0" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"}, - {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"}, - {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"}, - {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"}, - {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"}, - {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"}, - {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"}, - {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"}, - {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"}, - {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"}, - {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"}, - {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"}, - {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"}, - {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"}, - {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"}, - {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"}, - {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"}, - {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"}, - {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"}, - {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"}, - {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"}, - {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"}, - {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"}, - {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"}, - {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, + {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, + {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, + {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, + {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, + {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, + {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, + {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, + {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, + {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, ] [package.dependencies] @@ -2831,49 +2524,38 @@ cycler = ">=0.10" fonttools = ">=4.22.0" importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.3.1" -numpy = ">=1.21" +numpy = ">=1.23" packaging = ">=20.0" pillow = ">=8" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + [[package]] name = "matplotlib-inline" -version = "0.1.6" +version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] [package.dependencies] traitlets = "*" -[[package]] -name = "matplotlib-scalebar" -version = "0.8.1" -description = "Artist for matplotlib to display a scale bar" -optional = false -python-versions = "~=3.7" -files = [ - {file = "matplotlib-scalebar-0.8.1.tar.gz", hash = "sha256:14887af1093579c5e6afae51a0a1ecc3f715cdbc5c4d7ef59cdeec76ee6bb15d"}, - {file = "matplotlib_scalebar-0.8.1-py2.py3-none-any.whl", hash = "sha256:a8a2f361d4c2d576d087df3092ed95cac2f708f8b40d5d2bb992bd190e740b3a"}, -] - -[package.dependencies] -matplotlib = "*" - [[package]] name = "mdit-py-plugins" -version = "0.4.0" +version = "0.4.1" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" files = [ - {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, - {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, + {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, + {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, ] [package.dependencies] @@ -2917,122 +2599,40 @@ files = [ {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, ] -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "msgpack" -version = "1.0.8" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] @@ -3059,17 +2659,17 @@ files = [ [[package]] name = "myst-parser" -version = "2.0.0" +version = "3.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.8" files = [ - {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, - {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, + {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, + {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, ] [package.dependencies] -docutils = ">=0.16,<0.21" +docutils = ">=0.18,<0.22" jinja2 = "*" markdown-it-py = ">=3.0,<4.0" mdit-py-plugins = ">=0.4,<1.0" @@ -3079,9 +2679,9 @@ sphinx = ">=6,<8" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] +rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "natsort" @@ -3100,33 +2700,20 @@ icu = ["PyICU (>=1.0.0)"] [[package]] name = "nbclassic" -version = "1.0.0" +version = "1.1.0" description = "Jupyter Notebook as a Jupyter Server extension." optional = false python-versions = ">=3.7" files = [ - {file = "nbclassic-1.0.0-py3-none-any.whl", hash = "sha256:f99e4769b4750076cd4235c044b61232110733322384a94a63791d2e7beacc66"}, - {file = "nbclassic-1.0.0.tar.gz", hash = "sha256:0ae11eb2319455d805596bf320336cda9554b41d99ab9a3c31bf8180bffa30e3"}, + {file = "nbclassic-1.1.0-py3-none-any.whl", hash = "sha256:8c0fd6e36e320a18657ff44ed96c3a400f17a903a3744fc322303a515778f2ba"}, + {file = "nbclassic-1.1.0.tar.gz", hash = "sha256:77b77ba85f9e988f9bad85df345b514e9e64c7f0e822992ab1df4a78ac64fc1e"}, ] [package.dependencies] -argon2-cffi = "*" ipykernel = "*" ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=6.1.1" -jupyter-core = ">=4.6.1" -jupyter-server = ">=1.8" -nbconvert = ">=5" -nbformat = "*" nest-asyncio = ">=1.5" notebook-shim = ">=0.2.3" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" [package.extras] docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] @@ -3157,13 +2744,13 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.16.3" +version = "7.16.4" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.16.3-py3-none-any.whl", hash = "sha256:ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b"}, - {file = "nbconvert-7.16.3.tar.gz", hash = "sha256:a6733b78ce3d47c3f85e504998495b07e6ea9cf9bf6ec1c98dda63ec6ad19142"}, + {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, + {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, ] [package.dependencies] @@ -3185,9 +2772,9 @@ tinycss2 = "*" traitlets = ">=5.1" [package.extras] -all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["nbconvert[qtpng]"] +qtpdf = ["pyqtwebengine (>=5.15)"] qtpng = ["pyqtwebengine (>=5.15)"] serve = ["tornado (>=6.1)"] test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] @@ -3216,37 +2803,23 @@ test = ["pep440", "pre-commit", "pytest", "testpath"] [[package]] name = "nbsphinx" -version = "0.9.3" +version = "0.9.4" description = "Jupyter Notebook Tools for Sphinx" optional = false python-versions = ">=3.6" files = [ - {file = "nbsphinx-0.9.3-py3-none-any.whl", hash = "sha256:6e805e9627f4a358bd5720d5cbf8bf48853989c79af557afd91a5f22e163029f"}, - {file = "nbsphinx-0.9.3.tar.gz", hash = "sha256:ec339c8691b688f8676104a367a4b8cf3ea01fd089dc28d24dec22d563b11562"}, + {file = "nbsphinx-0.9.4-py3-none-any.whl", hash = "sha256:22cb1d974a8300e8118ca71aea1f649553743c0c5830a54129dcd446e6a8ba17"}, + {file = "nbsphinx-0.9.4.tar.gz", hash = "sha256:042a60806fc23d519bc5bef59d95570713913fe442fda759d53e3aaf62104794"}, ] [package.dependencies] -docutils = "*" +docutils = ">=0.18.1" jinja2 = "*" -nbconvert = "!=5.4" +nbconvert = ">=5.3,<5.4 || >5.4" nbformat = "*" sphinx = ">=1.8" traitlets = ">=5" -[[package]] -name = "ndindex" -version = "1.8" -description = "A Python library for manipulating indices of ndarrays." -optional = false -python-versions = ">=3.8" -files = [ - {file = "ndindex-1.8-py3-none-any.whl", hash = "sha256:b5132cd331f3e4106913ed1a974a3e355967a5991543c2f512b40cb8bb9f50b8"}, - {file = "ndindex-1.8.tar.gz", hash = "sha256:5fc87ebc784605f01dd5367374cb40e8da8f2c30988968990066c5098a7eebe8"}, -] - -[package.extras] -arrays = ["numpy"] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -3277,98 +2850,25 @@ extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.1 test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] -name = "niondata" -version = "15.6.2" -description = "A data processing library for Nion Swift." -optional = false -python-versions = ">=3.9" -files = [ - {file = "niondata-15.6.2-py3-none-any.whl", hash = "sha256:62cee1895b3993e1e13a017121f2804b362697b6bd831833d6335566a0a89e64"}, - {file = "niondata-15.6.2.tar.gz", hash = "sha256:1298e680ff6dcbecfff61451a8568920690dba9c5ac3966695d9e1442ca4d546"}, -] - -[package.dependencies] -nionutils = ">=0.4.5,<5.0.0" -numpy = ">=1.21,<2.0" -scipy = "*" - -[[package]] -name = "nionswift" -version = "16.10.0" -description = "Nion Swift: Scientific Image Processing." +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" optional = false -python-versions = ">=3.9" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nionswift-16.10.0-py3-none-any.whl", hash = "sha256:6c40b770bdeffaa24e6d9075c843e813530e566f148210a7f706f06ceab16bde"}, - {file = "nionswift-16.10.0.tar.gz", hash = "sha256:1e3c6d7fbe806b50b5ffd80c8c6352fa9e1c3f6f61d6c426bcda0c8729dc558a"}, + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[package.dependencies] -h5py = "*" -imageio = ">=2.19.0" -niondata = ">=15.6.2,<16.0" -nionswift-io = ">=15.2,<16.0" -nionui = ">=7.0.2,<8.0" -nionutils = ">=0.4.10,<5.0" -numpy = ">=1.26,<2.0" -pillow = "*" -pytz = "*" -scipy = "*" -tzlocal = "*" - [[package]] -name = "nionswift-io" -version = "15.2.1" -description = "NionSwift IO handlers." +name = "notebook" +version = "6.5.7" +description = "A web-based notebook environment for interactive computing" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" files = [ - {file = "nionswift-io-15.2.1.tar.gz", hash = "sha256:d40696c8802d67c348eed69bb6882f088b99f9adefc53c1c26701eca725c1d18"}, - {file = "nionswift_io-15.2.1-py3-none-any.whl", hash = "sha256:602fd74ba561929ed4317a32393461e1c8cb9091b94c40703a41092042fe4697"}, -] - -[package.dependencies] -imageio = "*" -niondata = ">=15.6,<16.0" -nionutils = ">=0.4.0,<5.0.0" -numpy = ">=1.25,<2.0" - -[[package]] -name = "nionui" -version = "7.0.3" -description = "Nion UI framework." -optional = false -python-versions = ">=3.9" -files = [ - {file = "nionui-7.0.3-py3-none-any.whl", hash = "sha256:ca3a8e78f527ac641183b2e3936bbba26f15c051e6cc1b7a05242bf1ce5307e9"}, - {file = "nionui-7.0.3.tar.gz", hash = "sha256:1d81ec9ace0d4df1ec1731a32f72aff66860ccbdbb88098f7b964cd18e35a434"}, -] - -[package.dependencies] -imageio = ">=2.19.0" -nionutils = ">=0.4.7,<5.0.0" -numpy = ">=1.21,<2.0" - -[[package]] -name = "nionutils" -version = "0.4.10" -description = "Nion utility classes." -optional = false -python-versions = ">=3.9" -files = [ - {file = "nionutils-0.4.10-py3-none-any.whl", hash = "sha256:7eea639227a621f727b951e06bfdca00ec50767a3a16dcb1918121dd32387671"}, - {file = "nionutils-0.4.10.tar.gz", hash = "sha256:cd74a23d10a45c3d24ec2bd5b766b28ee35d42ec5ed5aa730442321f13337fed"}, -] - -[[package]] -name = "notebook" -version = "6.5.6" -description = "A web-based notebook environment for interactive computing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "notebook-6.5.6-py3-none-any.whl", hash = "sha256:c1e2eb2e3b6079a0552a04974883a48d04c3c05792170d64a4b23d707d453181"}, - {file = "notebook-6.5.6.tar.gz", hash = "sha256:b4625a4b7a597839dd3156b140d5ba2c7123761f98245a3290f67a8b8ee048d9"}, + {file = "notebook-6.5.7-py3-none-any.whl", hash = "sha256:a6afa9a4ff4d149a0771ff8b8c881a7a73b3835f9add0606696d6e9d98ac1cd0"}, + {file = "notebook-6.5.7.tar.gz", hash = "sha256:04eb9011dfac634fbd4442adaf0a8c27cd26beef831fe1d19faf930c327768e4"}, ] [package.dependencies] @@ -3383,7 +2883,7 @@ nbconvert = ">=5" nbformat = "*" nest-asyncio = ">=1.5" prometheus-client = "*" -pyzmq = ">=17,<25" +pyzmq = ">=17" Send2Trash = ">=1.8.0" terminado = ">=0.8.3" tornado = ">=6.1" @@ -3413,118 +2913,37 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numba" -version = "0.59.1" +version = "0.60.0" description = "compiling Python code using LLVM" optional = false python-versions = ">=3.9" files = [ - {file = "numba-0.59.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97385a7f12212c4f4bc28f648720a92514bee79d7063e40ef66c2d30600fd18e"}, - {file = "numba-0.59.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b77aecf52040de2a1eb1d7e314497b9e56fba17466c80b457b971a25bb1576d"}, - {file = "numba-0.59.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3476a4f641bfd58f35ead42f4dcaf5f132569c4647c6f1360ccf18ee4cda3990"}, - {file = "numba-0.59.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:525ef3f820931bdae95ee5379c670d5c97289c6520726bc6937a4a7d4230ba24"}, - {file = "numba-0.59.1-cp310-cp310-win_amd64.whl", hash = "sha256:990e395e44d192a12105eca3083b61307db7da10e093972ca285c85bef0963d6"}, - {file = "numba-0.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43727e7ad20b3ec23ee4fc642f5b61845c71f75dd2825b3c234390c6d8d64051"}, - {file = "numba-0.59.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:411df625372c77959570050e861981e9d196cc1da9aa62c3d6a836b5cc338966"}, - {file = "numba-0.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2801003caa263d1e8497fb84829a7ecfb61738a95f62bc05693fcf1733e978e4"}, - {file = "numba-0.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd2842fac03be4e5324ebbbd4d2d0c8c0fc6e0df75c09477dd45b288a0777389"}, - {file = "numba-0.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:0594b3dfb369fada1f8bb2e3045cd6c61a564c62e50cf1f86b4666bc721b3450"}, - {file = "numba-0.59.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1cce206a3b92836cdf26ef39d3a3242fec25e07f020cc4feec4c4a865e340569"}, - {file = "numba-0.59.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c8b4477763cb1fbd86a3be7050500229417bf60867c93e131fd2626edb02238"}, - {file = "numba-0.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d80bce4ef7e65bf895c29e3889ca75a29ee01da80266a01d34815918e365835"}, - {file = "numba-0.59.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7ad1d217773e89a9845886401eaaab0a156a90aa2f179fdc125261fd1105096"}, - {file = "numba-0.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bf68f4d69dd3a9f26a9b23548fa23e3bcb9042e2935257b471d2a8d3c424b7f"}, - {file = "numba-0.59.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e0318ae729de6e5dbe64c75ead1a95eb01fabfe0e2ebed81ebf0344d32db0ae"}, - {file = "numba-0.59.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f68589740a8c38bb7dc1b938b55d1145244c8353078eea23895d4f82c8b9ec1"}, - {file = "numba-0.59.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:649913a3758891c77c32e2d2a3bcbedf4a69f5fea276d11f9119677c45a422e8"}, - {file = "numba-0.59.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9712808e4545270291d76b9a264839ac878c5eb7d8b6e02c970dc0ac29bc8187"}, - {file = "numba-0.59.1-cp39-cp39-win_amd64.whl", hash = "sha256:8d51ccd7008a83105ad6a0082b6a2b70f1142dc7cfd76deb8c5a862367eb8c86"}, - {file = "numba-0.59.1.tar.gz", hash = "sha256:76f69132b96028d2774ed20415e8c528a34e3299a40581bae178f0994a2f370b"}, -] - -[package.dependencies] -llvmlite = "==0.42.*" -numpy = ">=1.22,<1.27" - -[[package]] -name = "numcodecs" -version = "0.12.1" -description = "A Python package providing buffer compression and transformation codecs for use in data storage and communication applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "numcodecs-0.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d37f628fe92b3699e65831d5733feca74d2e33b50ef29118ffd41c13c677210e"}, - {file = "numcodecs-0.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:941b7446b68cf79f089bcfe92edaa3b154533dcbcd82474f994b28f2eedb1c60"}, - {file = "numcodecs-0.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e79bf9d1d37199ac00a60ff3adb64757523291d19d03116832e600cac391c51"}, - {file = "numcodecs-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:82d7107f80f9307235cb7e74719292d101c7ea1e393fe628817f0d635b7384f5"}, - {file = "numcodecs-0.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eeaf42768910f1c6eebf6c1bb00160728e62c9343df9e2e315dc9fe12e3f6071"}, - {file = "numcodecs-0.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:135b2d47563f7b9dc5ee6ce3d1b81b0f1397f69309e909f1a35bb0f7c553d45e"}, - {file = "numcodecs-0.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a191a8e347ecd016e5c357f2bf41fbcb026f6ffe78fff50c77ab12e96701d155"}, - {file = "numcodecs-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:21d8267bd4313f4d16f5b6287731d4c8ebdab236038f29ad1b0e93c9b2ca64ee"}, - {file = "numcodecs-0.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2f84df6b8693206365a5b37c005bfa9d1be486122bde683a7b6446af4b75d862"}, - {file = "numcodecs-0.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:760627780a8b6afdb7f942f2a0ddaf4e31d3d7eea1d8498cf0fd3204a33c4618"}, - {file = "numcodecs-0.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c258bd1d3dfa75a9b708540d23b2da43d63607f9df76dfa0309a7597d1de3b73"}, - {file = "numcodecs-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e04649ea504aff858dbe294631f098fbfd671baf58bfc04fc48d746554c05d67"}, - {file = "numcodecs-0.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:caf1a1e6678aab9c1e29d2109b299f7a467bd4d4c34235b1f0e082167846b88f"}, - {file = "numcodecs-0.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c17687b1fd1fef68af616bc83f896035d24e40e04e91e7e6dae56379eb59fe33"}, - {file = "numcodecs-0.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29dfb195f835a55c4d490fb097aac8c1bcb96c54cf1b037d9218492c95e9d8c5"}, - {file = "numcodecs-0.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:2f1ba2f4af3fd3ba65b1bcffb717fe65efe101a50a91c368f79f3101dbb1e243"}, - {file = "numcodecs-0.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fbb12a6a1abe95926f25c65e283762d63a9bf9e43c0de2c6a1a798347dfcb40"}, - {file = "numcodecs-0.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f2207871868b2464dc11c513965fd99b958a9d7cde2629be7b2dc84fdaab013b"}, - {file = "numcodecs-0.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abff3554a6892a89aacf7b642a044e4535499edf07aeae2f2e6e8fc08c9ba07f"}, - {file = "numcodecs-0.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef964d4860d3e6b38df0633caf3e51dc850a6293fd8e93240473642681d95136"}, - {file = "numcodecs-0.12.1.tar.gz", hash = "sha256:05d91a433733e7eef268d7e80ec226a0232da244289614a8f3826901aec1098e"}, -] - -[package.dependencies] -numpy = ">=1.7" - -[package.extras] -docs = ["mock", "numpydoc", "sphinx (<7.0.0)", "sphinx-issues"] -msgpack = ["msgpack"] -test = ["coverage", "flake8", "pytest", "pytest-cov"] -test-extras = ["importlib-metadata"] -zfpy = ["zfpy (>=1.0.0)"] - -[[package]] -name = "numexpr" -version = "2.10.0" -description = "Fast numerical expression evaluator for NumPy" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numexpr-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1af6dc6b3bd2e11a802337b352bf58f30df0b70be16c4f863b70a3af3a8ef95e"}, - {file = "numexpr-2.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c66dc0188358cdcc9465b6ee54fd5eef2e83ac64b1d4ba9117c41df59bf6fca"}, - {file = "numexpr-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83f1e7a7f7ee741b8dcd20c56c3f862a3a3ec26fa8b9fcadb7dcd819876d2f35"}, - {file = "numexpr-2.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f0b045e1831953a47cc9fabae76a6794c69cbb60921751a5cf2d555034c55bf"}, - {file = "numexpr-2.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d8eb88b0ae3d3c609d732a17e71096779b2bf47b3a084320ffa93d9f9132786"}, - {file = "numexpr-2.10.0-cp310-cp310-win32.whl", hash = "sha256:629b66cc1b750671e7fb396506b3f9410612e5bd8bc1dd55b5a0a0041d839f95"}, - {file = "numexpr-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:78e0a8bc4417c3dedcbae3c473505b69080535246edc977c7dccf3ec8454a685"}, - {file = "numexpr-2.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a602692cd52ce923ce8a0a90fb1d6cf186ebe8706eed83eee0de685e634b9aa9"}, - {file = "numexpr-2.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:745b46a1fb76920a3eebfaf26e50bc94a9c13b5aee34b256ab4b2d792dbaa9ca"}, - {file = "numexpr-2.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10789450032357afaeda4ac4d06da9542d1535c13151e8d32b49ae1a488d1358"}, - {file = "numexpr-2.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4feafc65ea3044b8bf8f305b757a928e59167a310630c22b97a57dff07a56490"}, - {file = "numexpr-2.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:937d36c6d3cf15601f26f84f0f706649f976491e9e0892d16cd7c876d77fa7dc"}, - {file = "numexpr-2.10.0-cp311-cp311-win32.whl", hash = "sha256:03d0ba492e484a5a1aeb24b300c4213ed168f2c246177be5733abb4e18cbb043"}, - {file = "numexpr-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:6b5f8242c075477156d26b3a6b8e0cd0a06d4c8eb68d907bde56dd3c9c683e92"}, - {file = "numexpr-2.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b276e2ba3e87ace9a30fd49078ad5dcdc6a1674d030b1ec132599c55465c0346"}, - {file = "numexpr-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb5e12787101f1216f2cdabedc3417748f2e1f472442e16bbfabf0bab2336300"}, - {file = "numexpr-2.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05278bad96b5846d712eba58b44e5cec743bdb3e19ca624916c921d049fdbcf6"}, - {file = "numexpr-2.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cdf9e64c5b3dbb61729edb505ea75ee212fa02b85c5b1d851331381ae3b0e1"}, - {file = "numexpr-2.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e3a973265591b0a875fd1151c4549e468959c7192821aac0bb86937694a08efa"}, - {file = "numexpr-2.10.0-cp312-cp312-win32.whl", hash = "sha256:416e0e9f0fc4cced67767585e44cb6b301728bdb9edbb7c534a853222ec62cac"}, - {file = "numexpr-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:748e8d4cde22d9a5603165293fb293a4de1a4623513299416c64fdab557118c2"}, - {file = "numexpr-2.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc3506c30c03b082da2cadef43747d474e5170c1f58a6dcdf882b3dc88b1e849"}, - {file = "numexpr-2.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:efa63ecdc9fcaf582045639ddcf56e9bdc1f4d9a01729be528f62df4db86c9d6"}, - {file = "numexpr-2.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a64d0dd8f8e694da3f8582d73d7da8446ff375f6dd239b546010efea371ac3"}, - {file = "numexpr-2.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d47bb567e330ebe86781864219a36cbccb3a47aec893bd509f0139c6b23e8104"}, - {file = "numexpr-2.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c7517b774d309b1f0896c89bdd1ddd33c4418a92ecfbe5e1df3ac698698f6fcf"}, - {file = "numexpr-2.10.0-cp39-cp39-win32.whl", hash = "sha256:04e8620e7e676504201d4082e7b3ee2d9b561d1cb9470b47a6104e10c1e2870e"}, - {file = "numexpr-2.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:56d0d96b130f7cd4d78d0017030d6a0e9d9fc2a717ac51d4cf4860b39637e86a"}, - {file = "numexpr-2.10.0.tar.gz", hash = "sha256:c89e930752639df040539160326d8f99a84159bbea41943ab8e960591edaaef0"}, -] - -[package.dependencies] -numpy = ">=1.19.3" + {file = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"}, + {file = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"}, + {file = "numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781"}, + {file = "numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e"}, + {file = "numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198"}, + {file = "numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8"}, + {file = "numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b"}, + {file = "numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703"}, + {file = "numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8"}, + {file = "numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2"}, + {file = "numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404"}, + {file = "numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c"}, + {file = "numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e"}, + {file = "numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d"}, + {file = "numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347"}, + {file = "numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74"}, + {file = "numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449"}, + {file = "numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b"}, + {file = "numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25"}, + {file = "numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab"}, + {file = "numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16"}, +] + +[package.dependencies] +llvmlite = "==0.43.*" +numpy = ">=1.22,<2.1" [[package]] name = "numpy" @@ -3571,197 +2990,96 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] -[[package]] -name = "numpy-quaternion" -version = "2023.0.3" -description = "Add a quaternion dtype to NumPy" -optional = false -python-versions = "*" -files = [ - {file = "numpy-quaternion-2023.0.3.tar.gz", hash = "sha256:392bf3cb4eee36c0e9271534e93e39e46cdb4f7e2062b08cb38bd0872061ff6c"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7fb35558e572d17ede74bd295bd39b7b4e430e2454394162a419ec13e398e16f"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fcac49e60f26be6db809fec0ce07b36af48cea159c7d5ac4944955814846b3b4"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33fa45a2a7e52173ecc10862377dcccc97b6f97c84aabb70e922d29411836694"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a16cc15587c47fd5ba512c621b2f72e9f7c26b829ef869395f9d16029639849e"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24aee35a72195c3f4002859c48f9a77660385153ea72df7ef3a9dc9c5e6e533"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05f00ce120089315d67471e01542e9aeffa3069be05e41690d6de5ac36a1da0"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:82158827e53fe84b79bb0e1fa993151dd4b25381c148d0486fe0518f96937c55"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:00462f35b7668e45ec626578cbb7f67c6a258b915d77de606e4f5058c882ed4d"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-win32.whl", hash = "sha256:241d635791b5aadb798a56baba0d754b8f5b11ac93e6e490de52324dd06ee608"}, - {file = "numpy_quaternion-2023.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f54a63b34f4207f7bf56dda94aecb7926019ba99d860e305048cb4ec126dcd14"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3aec1175b788b74d788fdef7c221707a2f68cbc20653ddc2a3c15e5332c1fb4"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:734259bfa8da467812a46ed79fcd9e3429e9aaa26f4449f47e65a709abd8b000"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77d98ea3a5c6d005dbda7cf38c3d8bf5de49fa3b5809046083349439930248d0"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:324d19204d7ce7b60cfd9b1a351eb2e6bbfda307e5ea8326a2978f5dc082f334"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6175a0555b9e6cf8fb22f88113b55e29dbec26622938e61a79ea4fcfcd8ad8e3"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ebc517a59487e2424b98a0d6291d1e7ad77ca36d2702015357f62097953de1f"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6699dbba41286da391b1ba40d0bebab8c2417cbce799d70c5745a254630bd0cd"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:73e10823b0b0fb0f5f1a5fb6e36db3e09d5ed1b547dd5e6a31ac2f126916dfe5"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-win32.whl", hash = "sha256:151f238ad5bfe51e18a8fad6afc75bec060f9ec3ebd01ad0565fb33ab6714818"}, - {file = "numpy_quaternion-2023.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:cd6bed4d09c141062d6e3745e0fcaae80536a36ffa5ba3248771a7ca8d447734"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7c67e35c3f128f8c32b76be448d8b8f102d4fd8d19086afc816fa8908abd91f0"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0ba2e4f266a94650d45407ff8507c546951f30a445f3a3f1701e517c6c456dda"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4285e8e4df7216f9adfe15d2802472c89758d92cbf3c7fde31798ca0e22eb7e0"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:481d386e4863e2aef88b618ac73e6dec9635737d01c9ccddfd53a855edcfea84"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92be260eccb718f07c67e314601a3cca22bb5cdcba8e6fd6c70febd24f947d4a"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e56939aae3f493acfa7c09dc5476893a61b3a7843e7c22d7cf65a14f9fe5eba3"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-win32.whl", hash = "sha256:1b8fc0d8cdee31cee21d8564726dab3ad8206a4119d60e0732bcd9b8b7d079d2"}, - {file = "numpy_quaternion-2023.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:23f429263e0e6f290ca6f6187a2739567f7fcd5e066259f0098007fa06068d44"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85811e0dbf7bb5e0d1d589331ecf452f972f555c1d6be31f3383c5e826184735"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:13cbd13e3119bbbc2e9c45cae27d5723f94ab758256eaafe75f7bf4deea6537f"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ab8912509ff8b6de23c39fa6ae2e6b055893687939382ba2289078f9c8d0f164"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2eef9417c0c7d339846537ce539417aff0e546f05c9c52ae4494966322e0ce7"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6992f33b7f0b94a25ff31685eb5ab13d173ccdb996a40b28757da4120e852930"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db804fed5bc128f4ff143bedf5ae987efc1f8c75b53956cc3f1194ed8732b4b8"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e3579c10c63a40fc466fa051419222fe17010c7ddabcd6300d7ae8ecdf4f68cc"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2f300ef729e3f0755e5518ef97770c27f29ebd5a1e58699adc2f045aad6958d"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-win32.whl", hash = "sha256:3489f172485bb86c7f1875f35a885ebdc671e3b5d45eac0e227d141b36a5bf02"}, - {file = "numpy_quaternion-2023.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:fadc1caa87ac0d7e898250fe30d92e83aa3e0f441edba106d1bffa54b684054e"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0dba8b77df485ca9fdc9b1e8bf57fcbc34e9f08362b73a29df2d5bd72c50fe7"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05c7b17fea1304e3f3aad8478148c671940abe6303c904c12da472ebb4e3c201"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d65d06b1a8c4f153f3b830601e8c1a1c3d75a92b289c67f5394684de082b36ef"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68acbb54ea114b258259a588a3de2b190f58855ff6289a71efb26a22f659e863"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4e22488ce2a5f4d8b93aafa50d2d659719b46fb358648853d52db12d000f13c"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e452a95a55bb58016ff5b555cde3ffcf95a4a4ad4d6c92b8f05119324f3229"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:48693acbbb5868d56a370bafd1051d1c7c7ab39f1b852a29e67ac1258938cf4d"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55ff80e4a2d6ca5d0e0e00e2ead59757e2816b50e97a19307ea09b56124bf5b1"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-win32.whl", hash = "sha256:1a99125c750ad5c7d17bf4c69b03fb9c86e418c582c9991ef9c119fd97a8a640"}, - {file = "numpy_quaternion-2023.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ed5667e9dd4c16f4d88407f041009374fd53c034b3d9b61a7496a515fb235a9b"}, -] - -[package.dependencies] -numpy = ">=1.13" - -[package.extras] -docs = ["mkdocs", "mktheapidocs[plugin]", "pymdown-extensions"] -numba = ["llvmlite (<0.32.0)", "numba", "numba (<0.49.0)"] -scipy = ["scipy"] -testing = ["pytest", "pytest-cov"] - [[package]] name = "opencv-python" -version = "4.9.0.80" +version = "4.10.0.84" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, ] [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] -[[package]] -name = "orix" -version = "0.11.1" -description = "orix is an open-source Python library for handling crystal orientation mapping data." -optional = false -python-versions = ">=3.7" -files = [ - {file = "orix-0.11.1-py3-none-any.whl", hash = "sha256:68e0863ee14ee446386a884e092ffa0683efb5d9d39125f8e63fdb5123cbbcdf"}, - {file = "orix-0.11.1.tar.gz", hash = "sha256:ab4c52d06cdc015347bc2025b67f7a3c0b9c419ec1b74e25a579fad8f13546a0"}, -] - -[package.dependencies] -dask = {version = "*", extras = ["array"]} -"diffpy.structure" = ">=3.0.2" -h5py = "*" -matplotlib = ">=3.3" -matplotlib-scalebar = "*" -numba = "*" -numpy = "*" -numpy-quaternion = "*" -pooch = ">=0.13" -scipy = "*" -tqdm = "*" - -[package.extras] -dev = ["black[jupyter]", "coverage (>=5.0)", "ipykernel", "isort (>=5.10)", "manifix", "memory-profiler", "nbsphinx (>=0.7)", "numpydoc", "outdated", "packaging", "pre-commit (>=1.16)", "pydata-sphinx-theme (>=0.13.1)", "pytest (>=5.4)", "pytest-cov (>=2.8.1)", "pytest-xdist", "scikit-image", "scikit-learn", "sphinx (>=3.0.2)", "sphinx-codeautolink[ipython]", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery (<0.11)", "sphinx-last-updated-by-git", "sphinxcontrib-bibtex (>=1.0)"] -doc = ["ipykernel", "memory-profiler", "nbsphinx (>=0.7)", "numpydoc", "pydata-sphinx-theme (>=0.13.1)", "scikit-image", "scikit-learn", "sphinx (>=3.0.2)", "sphinx-codeautolink[ipython]", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery (<0.11)", "sphinx-last-updated-by-git", "sphinxcontrib-bibtex (>=1.0)"] -tests = ["coverage (>=5.0)", "numpydoc", "pytest (>=5.4)", "pytest-cov (>=2.8.1)", "pytest-xdist"] - [[package]] name = "orjson" -version = "3.10.0" +version = "3.10.5" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47af5d4b850a2d1328660661f0881b67fdbe712aea905dadd413bdea6f792c33"}, - {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c90681333619d78360d13840c7235fdaf01b2b129cb3a4f1647783b1971542b6"}, - {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:400c5b7c4222cb27b5059adf1fb12302eebcabf1978f33d0824aa5277ca899bd"}, - {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5dcb32e949eae80fb335e63b90e5808b4b0f64e31476b3777707416b41682db5"}, - {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7d507c7493252c0a0264b5cc7e20fa2f8622b8a83b04d819b5ce32c97cf57b"}, - {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e286a51def6626f1e0cc134ba2067dcf14f7f4b9550f6dd4535fd9d79000040b"}, - {file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8acd4b82a5f3a3ec8b1dc83452941d22b4711964c34727eb1e65449eead353ca"}, - {file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:30707e646080dd3c791f22ce7e4a2fc2438765408547c10510f1f690bd336217"}, - {file = "orjson-3.10.0-cp310-none-win32.whl", hash = "sha256:115498c4ad34188dcb73464e8dc80e490a3e5e88a925907b6fedcf20e545001a"}, - {file = "orjson-3.10.0-cp310-none-win_amd64.whl", hash = "sha256:6735dd4a5a7b6df00a87d1d7a02b84b54d215fb7adac50dd24da5997ffb4798d"}, - {file = "orjson-3.10.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9587053e0cefc284e4d1cd113c34468b7d3f17666d22b185ea654f0775316a26"}, - {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bef1050b1bdc9ea6c0d08468e3e61c9386723633b397e50b82fda37b3563d72"}, - {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d16c6963ddf3b28c0d461641517cd312ad6b3cf303d8b87d5ef3fa59d6844337"}, - {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4251964db47ef090c462a2d909f16c7c7d5fe68e341dabce6702879ec26d1134"}, - {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73bbbdc43d520204d9ef0817ac03fa49c103c7f9ea94f410d2950755be2c349c"}, - {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:414e5293b82373606acf0d66313aecb52d9c8c2404b1900683eb32c3d042dbd7"}, - {file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feaed5bb09877dc27ed0d37f037ddef6cb76d19aa34b108db270d27d3d2ef747"}, - {file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5127478260db640323cea131ee88541cb1a9fbce051f0b22fa2f0892f44da302"}, - {file = "orjson-3.10.0-cp311-none-win32.whl", hash = "sha256:b98345529bafe3c06c09996b303fc0a21961820d634409b8639bc16bd4f21b63"}, - {file = "orjson-3.10.0-cp311-none-win_amd64.whl", hash = "sha256:658ca5cee3379dd3d37dbacd43d42c1b4feee99a29d847ef27a1cb18abdfb23f"}, - {file = "orjson-3.10.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4329c1d24fd130ee377e32a72dc54a3c251e6706fccd9a2ecb91b3606fddd998"}, - {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef0f19fdfb6553342b1882f438afd53c7cb7aea57894c4490c43e4431739c700"}, - {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4f60db24161534764277f798ef53b9d3063092f6d23f8f962b4a97edfa997a0"}, - {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1de3fd5c7b208d836f8ecb4526995f0d5877153a4f6f12f3e9bf11e49357de98"}, - {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f93e33f67729d460a177ba285002035d3f11425ed3cebac5f6ded4ef36b28344"}, - {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:237ba922aef472761acd697eef77fef4831ab769a42e83c04ac91e9f9e08fa0e"}, - {file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98c1bfc6a9bec52bc8f0ab9b86cc0874b0299fccef3562b793c1576cf3abb570"}, - {file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30d795a24be16c03dca0c35ca8f9c8eaaa51e3342f2c162d327bd0225118794a"}, - {file = "orjson-3.10.0-cp312-none-win32.whl", hash = "sha256:6a3f53dc650bc860eb26ec293dfb489b2f6ae1cbfc409a127b01229980e372f7"}, - {file = "orjson-3.10.0-cp312-none-win_amd64.whl", hash = "sha256:983db1f87c371dc6ffc52931eb75f9fe17dc621273e43ce67bee407d3e5476e9"}, - {file = "orjson-3.10.0-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a667769a96a72ca67237224a36faf57db0c82ab07d09c3aafc6f956196cfa1b"}, - {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade1e21dfde1d37feee8cf6464c20a2f41fa46c8bcd5251e761903e46102dc6b"}, - {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23c12bb4ced1c3308eff7ba5c63ef8f0edb3e4c43c026440247dd6c1c61cea4b"}, - {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2d014cf8d4dc9f03fc9f870de191a49a03b1bcda51f2a957943fb9fafe55aac"}, - {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eadecaa16d9783affca33597781328e4981b048615c2ddc31c47a51b833d6319"}, - {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd583341218826f48bd7c6ebf3310b4126216920853cbc471e8dbeaf07b0b80e"}, - {file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:90bfc137c75c31d32308fd61951d424424426ddc39a40e367704661a9ee97095"}, - {file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13b5d3c795b09a466ec9fcf0bd3ad7b85467d91a60113885df7b8d639a9d374b"}, - {file = "orjson-3.10.0-cp38-none-win32.whl", hash = "sha256:5d42768db6f2ce0162544845facb7c081e9364a5eb6d2ef06cd17f6050b048d8"}, - {file = "orjson-3.10.0-cp38-none-win_amd64.whl", hash = "sha256:33e6655a2542195d6fd9f850b428926559dee382f7a862dae92ca97fea03a5ad"}, - {file = "orjson-3.10.0-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4050920e831a49d8782a1720d3ca2f1c49b150953667eed6e5d63a62e80f46a2"}, - {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1897aa25a944cec774ce4a0e1c8e98fb50523e97366c637b7d0cddabc42e6643"}, - {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bf565a69e0082ea348c5657401acec3cbbb31564d89afebaee884614fba36b4"}, - {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6ebc17cfbbf741f5c1a888d1854354536f63d84bee537c9a7c0335791bb9009"}, - {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2817877d0b69f78f146ab305c5975d0618df41acf8811249ee64231f5953fee"}, - {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d017863ec8aa4589be30a328dacd13c2dc49de1c170bc8d8c8a98ece0f2925"}, - {file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22c2f7e377ac757bd3476ecb7480c8ed79d98ef89648f0176deb1da5cd014eb7"}, - {file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e62ba42bfe64c60c1bc84799944f80704e996592c6b9e14789c8e2a303279912"}, - {file = "orjson-3.10.0-cp39-none-win32.whl", hash = "sha256:60c0b1bdbccd959ebd1575bd0147bd5e10fc76f26216188be4a36b691c937077"}, - {file = "orjson-3.10.0-cp39-none-win_amd64.whl", hash = "sha256:175a41500ebb2fdf320bf78e8b9a75a1279525b62ba400b2b2444e274c2c8bee"}, - {file = "orjson-3.10.0.tar.gz", hash = "sha256:ba4d8cac5f2e2cff36bea6b6481cdb92b38c202bcec603d6f5ff91960595a1ed"}, + {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, + {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, + {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, + {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, + {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, + {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, + {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, + {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, + {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, + {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, + {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, + {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, + {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, + {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, + {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, + {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -3772,6 +3090,7 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -3785,12 +3104,14 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -3862,13 +3183,13 @@ testing = ["docopt", "pytest"] [[package]] name = "partd" -version = "1.4.1" +version = "1.4.2" description = "Appendable key-value storage" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "partd-1.4.1-py3-none-any.whl", hash = "sha256:27e766663d36c161e2827aa3e28541c992f0b9527d3cca047e13fb3acdb989e6"}, - {file = "partd-1.4.1.tar.gz", hash = "sha256:56c25dd49e6fea5727e731203c466c6e092f308d8f0024e199d02f6aa2167f67"}, + {file = "partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f"}, + {file = "partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c"}, ] [package.dependencies] @@ -3876,7 +3197,7 @@ locket = "*" toolz = "*" [package.extras] -complete = ["blosc", "numpy (>=1.9.0)", "pandas (>=0.19.0)", "pyzmq"] +complete = ["blosc", "numpy (>=1.20.0)", "pandas (>=1.3)", "pyzmq"] [[package]] name = "pexpect" @@ -4013,94 +3334,37 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa typing = ["typing-extensions"] xmp = ["defusedxml"] -[[package]] -name = "pint" -version = "0.23" -description = "Physical quantities module" -optional = false -python-versions = ">=3.9" -files = [ - {file = "Pint-0.23-py3-none-any.whl", hash = "sha256:df79b6b5f1beb7ed0cd55d91a0766fc55f972f757a9364e844958c05e8eb66f9"}, - {file = "Pint-0.23.tar.gz", hash = "sha256:e1509b91606dbc52527c600a4ef74ffac12fff70688aff20e9072409346ec9b4"}, -] - -[package.dependencies] -typing-extensions = "*" - -[package.extras] -babel = ["babel (<=2.8)"] -bench = ["pytest", "pytest-codspeed"] -dask = ["dask"] -mip = ["mip (>=1.13)"] -numpy = ["numpy (>=1.19.5)"] -pandas = ["pint-pandas (>=0.3)"] -test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mpl", "pytest-subtests"] -testbase = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-subtests"] -uncertainties = ["uncertainties (>=3.1.6)"] -xarray = ["xarray"] - [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -optional = false -python-versions = "*" -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - -[[package]] -name = "pooch" -version = "1.8.1" -description = "\"Pooch manages your Python library's sample data files: it automatically downloads and stores them in a local directory, with support for versioning and corruption checks.\"" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pooch-1.8.1-py3-none-any.whl", hash = "sha256:6b56611ac320c239faece1ac51a60b25796792599ce5c0b1bb87bf01df55e0a9"}, - {file = "pooch-1.8.1.tar.gz", hash = "sha256:27ef63097dd9a6e4f9d2694f5cfbf2f0a5defa44fccafec08d601e731d746270"}, -] - -[package.dependencies] -packaging = ">=20.0" -platformdirs = ">=2.5.0" -requests = ">=2.19.0" - -[package.extras] -progress = ["tqdm (>=4.41.0,<5.0.0)"] -sftp = ["paramiko (>=2.7.0)"] -xxhash = ["xxhash (>=1.4.3)"] - [[package]] name = "pprintpp" version = "0.4.0" @@ -4113,21 +3377,22 @@ files = [ ] [[package]] -name = "prettytable" -version = "3.10.0" -description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "prettytable-3.10.0-py3-none-any.whl", hash = "sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92"}, - {file = "prettytable-3.10.0.tar.gz", hash = "sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] -wcwidth = "*" - -[package.extras] -tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" [[package]] name = "prometheus-client" @@ -4145,13 +3410,13 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.43" +version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, - {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] @@ -4159,27 +3424,28 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.9.8" +version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, - {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, - {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, - {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, - {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, - {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, - {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, - {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, - {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, - {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, ] [package.extras] @@ -4210,90 +3476,53 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -description = "Get CPU info with pure Python" -optional = false -python-versions = "*" -files = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, -] - [[package]] name = "pyarrow" -version = "15.0.2" +version = "16.1.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, - {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e"}, - {file = "pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c"}, - {file = "pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4"}, - {file = "pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197"}, - {file = "pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38"}, - {file = "pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440"}, - {file = "pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b"}, - {file = "pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee"}, - {file = "pyarrow-15.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7167107d7fb6dcadb375b4b691b7e316f4368f39f6f45405a05535d7ad5e5058"}, - {file = "pyarrow-15.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e85241b44cc3d365ef950432a1b3bd44ac54626f37b2e3a0cc89c20e45dfd8bf"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:248723e4ed3255fcd73edcecc209744d58a9ca852e4cf3d2577811b6d4b59818"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ff3bdfe6f1b81ca5b73b70a8d482d37a766433823e0c21e22d1d7dde76ca33f"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f3d77463dee7e9f284ef42d341689b459a63ff2e75cee2b9302058d0d98fe142"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:8c1faf2482fb89766e79745670cbca04e7018497d85be9242d5350cba21357e1"}, - {file = "pyarrow-15.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:28f3016958a8e45a1069303a4a4f6a7d4910643fc08adb1e2e4a7ff056272ad3"}, - {file = "pyarrow-15.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:89722cb64286ab3d4daf168386f6968c126057b8c7ec3ef96302e81d8cdb8ae4"}, - {file = "pyarrow-15.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0ba387705044b3ac77b1b317165c0498299b08261d8122c96051024f953cd5"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2459bf1f22b6a5cdcc27ebfd99307d5526b62d217b984b9f5c974651398832"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58922e4bfece8b02abf7159f1f53a8f4d9f8e08f2d988109126c17c3bb261f22"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:adccc81d3dc0478ea0b498807b39a8d41628fa9210729b2f718b78cb997c7c91"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8bd2baa5fe531571847983f36a30ddbf65261ef23e496862ece83bdceb70420d"}, - {file = "pyarrow-15.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6669799a1d4ca9da9c7e06ef48368320f5856f36f9a4dd31a11839dda3f6cc8c"}, - {file = "pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9"}, -] - -[package.dependencies] -numpy = ">=1.16.6,<2" - -[[package]] -name = "pycifrw" -version = "4.4.6" -description = "CIF/STAR file support for Python" -optional = false -python-versions = "*" -files = [ - {file = "PyCifRW-4.4.6-cp311-cp311-manylinux_2_5_x86_64.whl", hash = "sha256:a89844ed5811700f995d1913a248d29d5745078ffd0f957b7e0574d74a48d0df"}, - {file = "PyCifRW-4.4.6.tar.gz", hash = "sha256:02bf5975e70ab71540bff62fbef3e8354ac707a0f0ab914a152047962891ef15"}, -] - -[package.dependencies] -numpy = "*" -ply = "*" + {file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"}, + {file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"}, + {file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"}, + {file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"}, + {file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"}, + {file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"}, + {file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"}, + {file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"}, + {file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"}, + {file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"}, + {file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"}, + {file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"}, + {file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"}, + {file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"}, + {file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"}, + {file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"}, +] + +[package.dependencies] +numpy = ">=1.16.6" [[package]] name = "pycparser" @@ -4308,22 +3537,22 @@ files = [ [[package]] name = "pyerfa" -version = "2.0.1.3" +version = "2.0.1.4" description = "Python bindings for ERFA" optional = false python-versions = ">=3.9" files = [ - {file = "pyerfa-2.0.1.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc554151de564b567e391b7c9c3b545efac63674ab1954382d38f886254c01fb"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:359327c88f1e5dea3974b284dabef141824ac54753c5cab6b3f23acd9d52071b"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c3a971a9fba8663b49dcc54c3419e837837140d81cc6be9f1c21fc56322f7b"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4472d2a2622e47d220a9436c953a487d8c051157f7b44b1f71964de17ee443b"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b0f621f26b5f31b3fb6bb113fb48a428e56eb00c7d729a242672dc4f886c8d18"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-win32.whl", hash = "sha256:b7a85ac9d807ea71550e831e873916ed3a44300fe6e20e0b3ca0f2784c0b2757"}, - {file = "pyerfa-2.0.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:60c0a73db5a42927fbafd12c623699c2c1b1233b6e1be1963970a5ad47e463c4"}, - {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:779caac3737da68f4db43b0dec026ac479719e02d25b8c4e7b0756abadbcd416"}, - {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:053ed25fdb7deb9d3d7cebecbb3d3dfbeea37c8c0011cc0616293e03d2c308eb"}, - {file = "pyerfa-2.0.1.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ef6c5d2206f134bd95329a0c17d46c449c9b68e9828e97e9bc43b29cd8789f5d"}, - {file = "pyerfa-2.0.1.3.tar.gz", hash = "sha256:ba5eb932341beaf222726de8dce2b1645c97b48c321efb2af8a535a7eb90ebfa"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win32.whl", hash = "sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win_amd64.whl", hash = "sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c"}, + {file = "pyerfa-2.0.1.4.tar.gz", hash = "sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3"}, ] [package.dependencies] @@ -4333,151 +3562,87 @@ numpy = ">=1.19" docs = ["sphinx-astropy (>=1.3)"] test = ["pytest", "pytest-doctestplus (>=0.7)"] -[[package]] -name = "pyfai" -version = "2024.2.0" -description = "Python implementation of fast azimuthal integration" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyfai-2024.2.0-cp310-cp310-macosx_10_9_arm64.whl", hash = "sha256:cd84cac53c3213c01bc35b6708262dd60e889e349486cb2d0f78a78e3a12c5c0"}, - {file = "pyfai-2024.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:088851ebcd1e753bf6e3a7a7ce3878475f7474e370925feb54dfc41c2804c896"}, - {file = "pyfai-2024.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6db0ff3135fe39d3a21b15a738a7fdde62d0d1c3bca096bbf9596ce3166d751c"}, - {file = "pyfai-2024.2.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec310d9b9b619d3b9ba3006b9b32d7c4ec6185f1dc352da4d26e122d42f78629"}, - {file = "pyfai-2024.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abffa58e5adda3ae3fe42aa253ba37054ccb01488a78e1ac8143c58287f97a41"}, - {file = "pyfai-2024.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b678812a6a93d9111d661feba9c1aba845a6eeb39c5911d2a7271940403d2a7"}, - {file = "pyfai-2024.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8bdb0e9c7e0086981981c58f285f351dff25d90e1137726fe6f79870b2c9369e"}, - {file = "pyfai-2024.2.0-cp311-cp311-macosx_10_9_arm64.whl", hash = "sha256:1284abd5cc858641fd51670adce4f898ccefed752fa0a570948b6867cebabcae"}, - {file = "pyfai-2024.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b409307aba0eb90a041fca2e636cfcea8f5b6d642b2578678f7a7294ab403df0"}, - {file = "pyfai-2024.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c78718c7f6153030e11f9e58ca375a090952a47b2475262d6346657e6434561"}, - {file = "pyfai-2024.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e3b44c345bdbeed28106aed8c1bf648fe10fdd3df2a7d97a892b8cb67124f27"}, - {file = "pyfai-2024.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7be06b71d422a6bb59a6f927816a10de3b95cd0752a81f4fd848f107ff4a177"}, - {file = "pyfai-2024.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:742a20cbd882dc029b405bb568334c6a2933222127dc0abab9ea74d72a243c90"}, - {file = "pyfai-2024.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:63dce8d02d9679be1798d8c59baf29aeb9b2de9374583bf93f7b66a851ee417d"}, - {file = "pyfai-2024.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:027e24f420358613cfef6d9df69c9d4863f5a7d5122f96868dd86a205bd8af0c"}, - {file = "pyfai-2024.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd4375ed84962575399ef2a165dedc3c7862d84a6da99375b483d2c8ce1c5a0"}, - {file = "pyfai-2024.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:615f196e3312c9918566798b38a3d7619dae1f0cba7dae237a0eeebf710103cd"}, - {file = "pyfai-2024.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:31a9a53d786fc0b711475f4de8ab20d639de9ac3bee7e66efb4037ec01cf8b8f"}, - {file = "pyfai-2024.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:387e84ba90d16486eda576312a162e187fb7c9208899cbfffea20f530a012790"}, - {file = "pyfai-2024.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ee408811eaae6755e99fa56e44783bf6709ac0de000e076bf91bddba7f26f4"}, - {file = "pyfai-2024.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb76e5fb842853ed3474c1ec2a1cdd06d946898ff12b660ab7cbd66c5f1432a5"}, - {file = "pyfai-2024.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d6b2ba800aaacbb91320bda6014740599d49fedc9063d48a2ab2f0b570487d38"}, - {file = "pyfai-2024.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b7a5403719216e0e2d9847974d4c12f03248e474f1ea0d5b02caca607621777"}, - {file = "pyfai-2024.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74680c8958524801cf6b9a75910b3c7e713475ed0587ff836b1140c67fc33e7a"}, - {file = "pyfai-2024.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a868bdd766f38245c9054f94fe41f535fe776d8611ddb838cd8becd342b57591"}, - {file = "pyfai-2024.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c71f9492b9c4bd2d46a84ea2cefa82731795bb197ceed565bad4ec4380ac7434"}, - {file = "pyfai-2024.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:823a407d3e03c2f0026af6d2350d8f41b8059d3ac66ecca996448167a8724a96"}, - {file = "pyfai-2024.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:9d8d637989fcd9971969e171206b5b23c9b05790ca8d6c9ba67c12d81af8a8aa"}, - {file = "pyfai-2024.2.0-cp39-cp39-macosx_10_9_arm64.whl", hash = "sha256:a04eaba4bc3d18bad6b1a296ffa3d5f0cfa934711011b2bb4f66efb34f5c834d"}, - {file = "pyfai-2024.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69f23b2e4460fb5edb989963855d567d5ca1c988ad8f731b099a0020584845fd"}, - {file = "pyfai-2024.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e3a9b68dc8280669cc35dac8bb3907cddfe749b56023e7e82d8fccf8f3f2208"}, - {file = "pyfai-2024.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0b941bd31853882ee40311632b9e67b233f8119b73611ce708007840233494f"}, - {file = "pyfai-2024.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b6f9fbca5fe68b14944a64b970dd9a6847a9cb678d32af965cf7346a727b2e9"}, - {file = "pyfai-2024.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71c105e28d160bf2a9a5f8bff35333b5143d22aa400c3e0d57d4e96b9cf73fb2"}, - {file = "pyfai-2024.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:b1ff9ce0c138b2ddf3878154360582b3f5785c59a53792a25759f17081ba2685"}, - {file = "pyfai-2024.2.0.tar.gz", hash = "sha256:6274b6ea7290a2415f0888c07585e6e46975c32b99007adab0701b2dc8f84894"}, -] - -[package.dependencies] -fabio = "*" -h5py = "*" -matplotlib = "*" -numexpr = "!=2.8.6" -numpy = ">=1.10" -scipy = "*" -silx = ">=2" - -[package.extras] -all = ["PyQt5", "hdf5plugin", "pyopencl"] -gui = ["PyQt5"] -opencl = ["pyopencl"] - [[package]] name = "pyfakefs" -version = "5.4.0" +version = "5.5.0" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ - {file = "pyfakefs-5.4.0-py3-none-any.whl", hash = "sha256:96e52554621a3af7b8171f8660debb65781bcd0cb0bdddea8b12e1b7871c33f3"}, - {file = "pyfakefs-5.4.0.tar.gz", hash = "sha256:969096d84b5b986f4f84399d03f4900381a3880d03adcdbd609566a4baf39bf9"}, + {file = "pyfakefs-5.5.0-py3-none-any.whl", hash = "sha256:8dbf203ab7bef1529f11f7d41b9478b898e95bf9f3b71262163aac07a518cd76"}, + {file = "pyfakefs-5.5.0.tar.gz", hash = "sha256:7448aaa07142f892d0a4eb52a5ed3206a9f02c6599e686cd97d624c18979c154"}, ] [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.1.1" +version = "0.3.2" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.1.1-py3-none-any.whl", hash = "sha256:5d2392ab55fa3c86c3572bcb879833987641bda7e8d7f9524505bf4beab11cd1"}, - {file = "pynxtools-0.1.1.tar.gz", hash = "sha256:5e0c3eb8b9cae7ed79a87ffb23a22edebb9d8d152a967009a43ef632ee6c421d"}, + {file = "pynxtools-0.3.2-py3-none-any.whl", hash = "sha256:c757fd8ef8e7150653c58271bd9b7fabede6068f344f78989cc7b0bb1da06e25"}, + {file = "pynxtools-0.3.2.tar.gz", hash = "sha256:6dc73ba82c50b7d0355cb2acb8679dc590dcf5bfea5174fa247d37f3a54466bf"}, ] [package.dependencies] +anytree = "*" ase = ">=3.19.0" click = ">=7.1.2" -flatdict = ">=4.0.1" -gitpython = ">=3.1.24" +click-default-group = "*" h5py = ">=3.6.0" -hyperspy = ">=1.7.4" -ifes-apt-tc-data-modeling = ">=0.0.9,<0.2" importlib-metadata = "*" -kikuchipy = ">=0.8.2" +lxml = ">=4.9.1" mergedeep = "*" -nionswift = ">=0.16.8" numpy = ">=1.21.2" pandas = ">=1.3.2" -pytz = ">=2021.1" -pyxem = ">=0.14.2" PyYAML = ">=6.0" -scipy = ">=1.7.1" -tzlocal = "<=4.3" xarray = ">=0.20.2" -zipfile37 = "0.1.3" [package.extras] -convert = ["pynxtools[mpes,stm,xps]"] -dev = ["mypy", "pip-tools", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff", "structlog", "types-pytz", "types-pyyaml", "types-requests"] +apm = ["pynxtools-apm"] +convert = ["pynxtools[apm,ellips,em,mpes,stm,xps,xrd]"] +dev = ["mypy", "pip-tools", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (==0.3.4)", "structlog", "types-pytz", "types-pyyaml", "types-requests"] docs = ["mkdocs", "mkdocs-macros-plugin", "mkdocs-material", "mkdocs-material-extensions"] +ellips = ["pynxtools-ellips"] +em = ["pynxtools-em"] mpes = ["pynxtools-mpes"] stm = ["pynxtools-stm"] xps = ["pynxtools-xps"] +xrd = ["pynxtools-xrd"] [[package]] name = "pynxtools-mpes" -version = "0.0.3" +version = "0.1.0" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-mpes-0.0.3.tar.gz", hash = "sha256:e0aaa0d4e0e69eec311b6e0fa7adff72759550f7847075fa548167541092059a"}, - {file = "pynxtools_mpes-0.0.3-py3-none-any.whl", hash = "sha256:9e5d9ccdffe69971e8819560e4a6bd9d39703cd9cfa9a8a7fd23121b2bca9ca6"}, + {file = "pynxtools_mpes-0.1.0-py3-none-any.whl", hash = "sha256:7632a5473b54435210605e330b0ffb014a57169594a20440cd88fc1667d3d407"}, + {file = "pynxtools_mpes-0.1.0.tar.gz", hash = "sha256:0ea950a5d84f25e1437ceb86c70e13b380a9f1b77b3cdcfc44b4ee569ee5232d"}, ] [package.dependencies] h5py = ">=3.6.0" -pynxtools = ">=0.0.10" +pynxtools = ">=0.3.2" PyYAML = ">=6.0" xarray = ">=0.20.2" [package.extras] -dev = ["mypy", "pip-tools", "pytest", "ruff", "types-pyyaml"] +dev = ["mypy", "pip-tools", "pytest", "ruff (==0.3.4)", "types-pyyaml"] [[package]] name = "pyparsing" @@ -4495,13 +3660,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -4509,11 +3674,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-clarity" @@ -4550,18 +3715,18 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-xdist" -version = "3.5.0" +version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, - {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, ] [package.dependencies] -execnet = ">=1.1" -pytest = ">=6.2.0" +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] @@ -4604,20 +3769,6 @@ files = [ {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] -[[package]] -name = "pytz-deprecation-shim" -version = "0.1.0.post0" -description = "Shims to make deprecation of pytz easier" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pytz_deprecation_shim-0.1.0.post0-py2.py3-none-any.whl", hash = "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6"}, - {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "python_version >= \"3.6\""} - [[package]] name = "pywin32" version = "306" @@ -4656,44 +3807,6 @@ files = [ {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, ] -[[package]] -name = "pyxem" -version = "0.16.0" -description = "multi-dimensional diffraction microscopy" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyxem-0.16.0-py3-none-any.whl", hash = "sha256:4e20cbbe5116eec69f344e549e041d401a6a0b5a69e54984875e0d3ab2466707"}, - {file = "pyxem-0.16.0.tar.gz", hash = "sha256:6be051f28530af117de27ff0f62c6843a9eaf96430bff18ba23ab16744b93684"}, -] - -[package.dependencies] -dask = "*" -diffsims = ">=0.5" -h5py = "*" -hyperspy = ">=1.7.0,<2.0rc0" -lmfit = ">=0.9.12" -matplotlib = ">=3.3" -numba = "*" -numexpr = "!=2.8.6" -numpy = "*" -orix = ">=0.9" -psutil = "*" -pyfai = "*" -scikit-image = ">=0.19.0,<0.21.0 || >0.21.0" -scikit-learn = ">=1.0" -scipy = "*" -tqdm = "*" -traits = "*" -transforms3d = "*" - -[package.extras] -dask = ["dask-image", "distributed"] -dev = ["black", "pre-commit (>=1.16)"] -doc = ["dask-image", "hyperspy-gui-ipywidgets", "nbsphinx (>=0.7)", "pydata-sphinx-theme", "sphinx (>=3.0.2)", "sphinx-autodoc-typehints (>=1.10.3)", "sphinx-codeautolink", "sphinx-copybutton (>=0.2.5)", "sphinx-design", "sphinx-gallery (>=0.6)", "sphinxcontrib-bibtex (>=1.0)"] -gpu = ["cupy (>=9.0.0)"] -tests = ["coverage (>=5.0)", "coveralls (>=1.10)", "pytest (>=5.0)", "pytest-cov (>=2.8.1)", "pytest-rerunfailures", "pytest-xdist"] - [[package]] name = "pyyaml" version = "6.0.1" @@ -4706,7 +3819,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -4714,15 +3826,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -4739,7 +3844,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -4747,7 +3851,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -4755,100 +3858,113 @@ files = [ [[package]] name = "pyzmq" -version = "24.0.1" +version = "26.0.3" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:28b119ba97129d3001673a697b7cce47fe6de1f7255d104c2f01108a5179a066"}, - {file = "pyzmq-24.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bcbebd369493d68162cddb74a9c1fcebd139dfbb7ddb23d8f8e43e6c87bac3a6"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae61446166983c663cee42c852ed63899e43e484abf080089f771df4b9d272ef"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f7ac99b15270db8d53f28c3c7b968612993a90a5cf359da354efe96f5372b4"}, - {file = "pyzmq-24.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca7c3956b03b7663fac4d150f5e6d4f6f38b2462c1e9afd83bcf7019f17913"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8c78bfe20d4c890cb5580a3b9290f700c570e167d4cdcc55feec07030297a5e3"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:48f721f070726cd2a6e44f3c33f8ee4b24188e4b816e6dd8ba542c8c3bb5b246"}, - {file = "pyzmq-24.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afe1f3bc486d0ce40abb0a0c9adb39aed3bbac36ebdc596487b0cceba55c21c1"}, - {file = "pyzmq-24.0.1-cp310-cp310-win32.whl", hash = "sha256:3e6192dbcefaaa52ed81be88525a54a445f4b4fe2fffcae7fe40ebb58bd06bfd"}, - {file = "pyzmq-24.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:86de64468cad9c6d269f32a6390e210ca5ada568c7a55de8e681ca3b897bb340"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:838812c65ed5f7c2bd11f7b098d2e5d01685a3f6d1f82849423b570bae698c00"}, - {file = "pyzmq-24.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfb992dbcd88d8254471760879d48fb20836d91baa90f181c957122f9592b3dc"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7abddb2bd5489d30ffeb4b93a428130886c171b4d355ccd226e83254fcb6b9ef"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94010bd61bc168c103a5b3b0f56ed3b616688192db7cd5b1d626e49f28ff51b3"}, - {file = "pyzmq-24.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8242543c522d84d033fe79be04cb559b80d7eb98ad81b137ff7e0a9020f00ace"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ccb94342d13e3bf3ffa6e62f95b5e3f0bc6bfa94558cb37f4b3d09d6feb536ff"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6640f83df0ae4ae1104d4c62b77e9ef39be85ebe53f636388707d532bee2b7b8"}, - {file = "pyzmq-24.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a180dbd5ea5d47c2d3b716d5c19cc3fb162d1c8db93b21a1295d69585bfddac1"}, - {file = "pyzmq-24.0.1-cp311-cp311-win32.whl", hash = "sha256:624321120f7e60336be8ec74a172ae7fba5c3ed5bf787cc85f7e9986c9e0ebc2"}, - {file = "pyzmq-24.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1724117bae69e091309ffb8255412c4651d3f6355560d9af312d547f6c5bc8b8"}, - {file = "pyzmq-24.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:15975747462ec49fdc863af906bab87c43b2491403ab37a6d88410635786b0f4"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b947e264f0e77d30dcbccbb00f49f900b204b922eb0c3a9f0afd61aaa1cedc3d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ec91f1bad66f3ee8c6deb65fa1fe418e8ad803efedd69c35f3b5502f43bd1dc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db03704b3506455d86ec72c3358a779e9b1d07b61220dfb43702b7b668edcd0d"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e7e66b4e403c2836ac74f26c4b65d8ac0ca1eef41dfcac2d013b7482befaad83"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7a23ccc1083c260fa9685c93e3b170baba45aeed4b524deb3f426b0c40c11639"}, - {file = "pyzmq-24.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fa0ae3275ef706c0309556061185dd0e4c4cd3b7d6f67ae617e4e677c7a41e2e"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win32.whl", hash = "sha256:f01de4ec083daebf210531e2cca3bdb1608dbbbe00a9723e261d92087a1f6ebc"}, - {file = "pyzmq-24.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:de4217b9eb8b541cf2b7fde4401ce9d9a411cc0af85d410f9d6f4333f43640be"}, - {file = "pyzmq-24.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:78068e8678ca023594e4a0ab558905c1033b2d3e806a0ad9e3094e231e115a33"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77c2713faf25a953c69cf0f723d1b7dd83827b0834e6c41e3fb3bbc6765914a1"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bb4af15f305056e95ca1bd086239b9ebc6ad55e9f49076d27d80027f72752f6"}, - {file = "pyzmq-24.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0f14cffd32e9c4c73da66db97853a6aeceaac34acdc0fae9e5bbc9370281864c"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0108358dab8c6b27ff6b985c2af4b12665c1bc659648284153ee501000f5c107"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d66689e840e75221b0b290b0befa86f059fb35e1ee6443bce51516d4d61b6b99"}, - {file = "pyzmq-24.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae08ac90aa8fa14caafc7a6251bd218bf6dac518b7bff09caaa5e781119ba3f2"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win32.whl", hash = "sha256:8421aa8c9b45ea608c205db9e1c0c855c7e54d0e9c2c2f337ce024f6843cab3b"}, - {file = "pyzmq-24.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54d8b9c5e288362ec8595c1d98666d36f2070fd0c2f76e2b3c60fbad9bd76227"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:acbd0a6d61cc954b9f535daaa9ec26b0a60a0d4353c5f7c1438ebc88a359a47e"}, - {file = "pyzmq-24.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:47b11a729d61a47df56346283a4a800fa379ae6a85870d5a2e1e4956c828eedc"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abe6eb10122f0d746a0d510c2039ae8edb27bc9af29f6d1b05a66cc2401353ff"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07bec1a1b22dacf718f2c0e71b49600bb6a31a88f06527dfd0b5aababe3fa3f7"}, - {file = "pyzmq-24.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d945a85b70da97ae86113faf9f1b9294efe66bd4a5d6f82f2676d567338b66"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1b7928bb7580736ffac5baf814097be342ba08d3cfdfb48e52773ec959572287"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b946da90dc2799bcafa682692c1d2139b2a96ec3c24fa9fc6f5b0da782675330"}, - {file = "pyzmq-24.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c8840f064b1fb377cffd3efeaad2b190c14d4c8da02316dae07571252d20b31f"}, - {file = "pyzmq-24.0.1-cp38-cp38-win32.whl", hash = "sha256:4854f9edc5208f63f0841c0c667260ae8d6846cfa233c479e29fdc85d42ebd58"}, - {file = "pyzmq-24.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:42d4f97b9795a7aafa152a36fe2ad44549b83a743fd3e77011136def512e6c2a"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:52afb0ac962963fff30cf1be775bc51ae083ef4c1e354266ab20e5382057dd62"}, - {file = "pyzmq-24.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bad8210ad4df68c44ff3685cca3cda448ee46e20d13edcff8909eba6ec01ca4"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dabf1a05318d95b1537fd61d9330ef4313ea1216eea128a17615038859da3b3b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5bd3d7dfd9cd058eb68d9a905dec854f86649f64d4ddf21f3ec289341386c44b"}, - {file = "pyzmq-24.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8012bce6836d3f20a6c9599f81dfa945f433dab4dbd0c4917a6fb1f998ab33d"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c31805d2c8ade9b11feca4674eee2b9cce1fec3e8ddb7bbdd961a09dc76a80ea"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3104f4b084ad5d9c0cb87445cc8cfd96bba710bef4a66c2674910127044df209"}, - {file = "pyzmq-24.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df0841f94928f8af9c7a1f0aaaffba1fb74607af023a152f59379c01c53aee58"}, - {file = "pyzmq-24.0.1-cp39-cp39-win32.whl", hash = "sha256:a435ef8a3bd95c8a2d316d6e0ff70d0db524f6037411652803e118871d703333"}, - {file = "pyzmq-24.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2032d9cb994ce3b4cba2b8dfae08c7e25bc14ba484c770d4d3be33c27de8c45b"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bb5635c851eef3a7a54becde6da99485eecf7d068bd885ac8e6d173c4ecd68b0"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:83ea1a398f192957cb986d9206ce229efe0ee75e3c6635baff53ddf39bd718d5"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:941fab0073f0a54dc33d1a0460cb04e0d85893cb0c5e1476c785000f8b359409"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8f482c44ccb5884bf3f638f29bea0f8dc68c97e38b2061769c4cb697f6140d"}, - {file = "pyzmq-24.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:613010b5d17906c4367609e6f52e9a2595e35d5cc27d36ff3f1b6fa6e954d944"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:65c94410b5a8355cfcf12fd600a313efee46ce96a09e911ea92cf2acf6708804"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:20e7eeb1166087db636c06cae04a1ef59298627f56fb17da10528ab52a14c87f"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2712aee7b3834ace51738c15d9ee152cc5a98dc7d57dd93300461b792ab7b43"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7c280185c4da99e0cc06c63bdf91f5b0b71deb70d8717f0ab870a43e376db8"}, - {file = "pyzmq-24.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:858375573c9225cc8e5b49bfac846a77b696b8d5e815711b8d4ba3141e6e8879"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:80093b595921eed1a2cead546a683b9e2ae7f4a4592bb2ab22f70d30174f003a"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f3f3154fde2b1ff3aa7b4f9326347ebc89c8ef425ca1db8f665175e6d3bd42f"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb756147314430bee5d10919b8493c0ccb109ddb7f5dfd2fcd7441266a25b75"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e706bac34e9f50779cb8c39f10b53a4d15aebb97235643d3112ac20bd577b4"}, - {file = "pyzmq-24.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:687700f8371643916a1d2c61f3fdaa630407dd205c38afff936545d7b7466066"}, - {file = "pyzmq-24.0.1.tar.gz", hash = "sha256:216f5d7dbb67166759e59b0479bca82b8acf9bed6015b526b8eb10143fb08e77"}, + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, + {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, + {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, + {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, + {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, + {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, + {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, + {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, + {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, + {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, + {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, + {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, + {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, + {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, + {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, + {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, + {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, + {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, + {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, + {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, + {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, + {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, + {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, + {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, + {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, + {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, + {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, + {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, + {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, + {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, + {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, + {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, + {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, + {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, ] [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} -py = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "qtconsole" -version = "5.5.1" +version = "5.5.2" description = "Jupyter Qt console" optional = true -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "qtconsole-5.5.1-py3-none-any.whl", hash = "sha256:8c75fa3e9b4ed884880ff7cea90a1b67451219279ec33deaee1d59e3df1a5d2b"}, - {file = "qtconsole-5.5.1.tar.gz", hash = "sha256:a0e806c6951db9490628e4df80caec9669b65149c7ba40f9bf033c025a5b56bc"}, + {file = "qtconsole-5.5.2-py3-none-any.whl", hash = "sha256:42d745f3d05d36240244a04e1e1ec2a86d5d9b6edb16dbdef582ccb629e87e0b"}, + {file = "qtconsole-5.5.2.tar.gz", hash = "sha256:6b5fb11274b297463706af84dcbbd5c92273b1f619e6d25d08874b0a88516989"}, ] [package.dependencies] @@ -4882,25 +3998,6 @@ packaging = "*" [package.extras] test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] -[[package]] -name = "radioactivedecay" -version = "0.4.22" -description = "A Python package for radioactive decay modelling that supports 1252 radionuclides, decay chains, branching, and metastable states." -optional = false -python-versions = ">=3.6" -files = [ - {file = "radioactivedecay-0.4.22-py3-none-any.whl", hash = "sha256:2d89bb69ced850a2c6725943fa608309658d773d224c484f335994bdcbe13bbf"}, - {file = "radioactivedecay-0.4.22.tar.gz", hash = "sha256:2aad5d3d8c91bce23b3fa129b01f26f40ff4aabc56382a84747a4a895a87de00"}, -] - -[package.dependencies] -matplotlib = "*" -networkx = "*" -numpy = "*" -scipy = "*" -setuptools = "*" -sympy = "*" - [[package]] name = "recommonmark" version = "0.7.1" @@ -4919,13 +4016,13 @@ sphinx = ">=1.3.1" [[package]] name = "referencing" -version = "0.34.0" +version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, - {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, ] [package.dependencies] @@ -4934,13 +4031,13 @@ rpds-py = ">=0.7.0" [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -5015,110 +4112,110 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.18.0" +version = "0.18.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, - {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, - {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, - {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, - {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, - {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, - {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, - {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, - {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, - {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, - {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, - {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, - {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, + {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, + {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, + {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, + {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, + {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, + {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, + {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, + {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, + {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, + {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, + {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, + {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, + {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, + {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, + {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, + {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, + {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, + {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, + {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, + {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, + {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, + {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, + {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, + {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, + {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, + {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, + {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, + {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, + {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, ] [[package]] @@ -5149,126 +4246,84 @@ files = [ [[package]] name = "scikit-image" -version = "0.22.0" +version = "0.24.0" description = "Image processing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d"}, - {file = "scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff"}, - {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6"}, - {file = "scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938"}, - {file = "scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc"}, - {file = "scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2"}, - {file = "scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2"}, - {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd"}, - {file = "scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e"}, - {file = "scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b"}, - {file = "scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff"}, - {file = "scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9"}, - {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452"}, - {file = "scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a"}, - {file = "scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2"}, - {file = "scikit_image-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22318b35044cfeeb63ee60c56fc62450e5fe516228138f1d06c7a26378248a86"}, - {file = "scikit_image-0.22.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9e801c44a814afdadeabf4dffdffc23733e393767958b82319706f5fa3e1eaa9"}, - {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c472a1fb3665ec5c00423684590631d95f9afcbc97f01407d348b821880b2cb3"}, - {file = "scikit_image-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b7a6c89e8d6252332121b58f50e1625c35f7d6a85489c0b6b7ee4f5155d547a"}, - {file = "scikit_image-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:5071b8f6341bfb0737ab05c8ab4ac0261f9e25dbcc7b5d31e5ed230fd24a7929"}, - {file = "scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236"}, -] - -[package.dependencies] -imageio = ">=2.27" -lazy_loader = ">=0.3" + {file = "scikit_image-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3bc0264b6ab30b43c4179ee6156bc18b4861e78bb329dd8d16537b7bbf827a"}, + {file = "scikit_image-0.24.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9c7a52e20cdd760738da38564ba1fed7942b623c0317489af1a598a8dedf088b"}, + {file = "scikit_image-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93f46e6ce42e5409f4d09ce1b0c7f80dd7e4373bcec635b6348b63e3c886eac8"}, + {file = "scikit_image-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ee0af13435c57351a3397eb379e72164ff85161923eec0c38849fecf1b4764"}, + {file = "scikit_image-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ac7913b028b8aa780ffae85922894a69e33d1c0bf270ea1774f382fe8bf95e7"}, + {file = "scikit_image-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:272909e02a59cea3ed4aa03739bb88df2625daa809f633f40b5053cf09241831"}, + {file = "scikit_image-0.24.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:190ebde80b4470fe8838764b9b15f232a964f1a20391663e31008d76f0c696f7"}, + {file = "scikit_image-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c98cc695005faf2b79904e4663796c977af22586ddf1b12d6af2fa22842dc2"}, + {file = "scikit_image-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa27b3a0dbad807b966b8db2d78da734cb812ca4787f7fbb143764800ce2fa9c"}, + {file = "scikit_image-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:dacf591ac0c272a111181afad4b788a27fe70d213cfddd631d151cbc34f8ca2c"}, + {file = "scikit_image-0.24.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6fccceb54c9574590abcddc8caf6cefa57c13b5b8b4260ab3ff88ad8f3c252b3"}, + {file = "scikit_image-0.24.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ccc01e4760d655aab7601c1ba7aa4ddd8b46f494ac46ec9c268df6f33ccddf4c"}, + {file = "scikit_image-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18836a18d3a7b6aca5376a2d805f0045826bc6c9fc85331659c33b4813e0b563"}, + {file = "scikit_image-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8579bda9c3f78cb3b3ed8b9425213c53a25fa7e994b7ac01f2440b395babf660"}, + {file = "scikit_image-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:82ab903afa60b2da1da2e6f0c8c65e7c8868c60a869464c41971da929b3e82bc"}, + {file = "scikit_image-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef04360eda372ee5cd60aebe9be91258639c86ae2ea24093fb9182118008d009"}, + {file = "scikit_image-0.24.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e9aadb442360a7e76f0c5c9d105f79a83d6df0e01e431bd1d5757e2c5871a1f3"}, + {file = "scikit_image-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e37de6f4c1abcf794e13c258dc9b7d385d5be868441de11c180363824192ff7"}, + {file = "scikit_image-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4688c18bd7ec33c08d7bf0fd19549be246d90d5f2c1d795a89986629af0a1e83"}, + {file = "scikit_image-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:56dab751d20b25d5d3985e95c9b4e975f55573554bd76b0aedf5875217c93e69"}, + {file = "scikit_image-0.24.0.tar.gz", hash = "sha256:5d16efe95da8edbeb363e0c4157b99becbd650a60b77f6e3af5768b66cf007ab"}, +] + +[package.dependencies] +imageio = ">=2.33" +lazy-loader = ">=0.4" networkx = ">=2.8" -numpy = ">=1.22" +numpy = ">=1.23" packaging = ">=21" -pillow = ">=9.0.1" -scipy = ">=1.8" +pillow = ">=9.1" +scipy = ">=1.9" tifffile = ">=2022.8.12" [package.extras] -build = ["Cython (>=0.29.32)", "build", "meson-python (>=0.14)", "ninja", "numpy (>=1.22)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.6)", "wheel"] +build = ["Cython (>=3.0.4)", "build", "meson-python (>=0.15)", "ninja", "numpy (>=2.0.0rc1)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.8)", "wheel"] data = ["pooch (>=1.6.0)"] -developer = ["pre-commit", "tomli"] -docs = ["PyWavelets (>=1.1.1)", "dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.5)", "myst-parser", "numpydoc (>=1.6)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.14.1)", "pytest-runner", "scikit-learn (>=1.1)", "seaborn (>=0.11)", "sphinx (>=7.2)", "sphinx-copybutton", "sphinx-gallery (>=0.14)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] -optional = ["PyWavelets (>=1.1.1)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.5)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=1.1)"] -test = ["asv", "matplotlib (>=3.5)", "numpydoc (>=1.5)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-faulthandler", "pytest-localserver"] - -[[package]] -name = "scikit-learn" -version = "1.4.2" -description = "A set of python modules for machine learning and data mining" -optional = false -python-versions = ">=3.9" -files = [ - {file = "scikit-learn-1.4.2.tar.gz", hash = "sha256:daa1c471d95bad080c6e44b4946c9390a4842adc3082572c20e4f8884e39e959"}, - {file = "scikit_learn-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8539a41b3d6d1af82eb629f9c57f37428ff1481c1e34dddb3b9d7af8ede67ac5"}, - {file = "scikit_learn-1.4.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:68b8404841f944a4a1459b07198fa2edd41a82f189b44f3e1d55c104dbc2e40c"}, - {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81bf5d8bbe87643103334032dd82f7419bc8c8d02a763643a6b9a5c7288c5054"}, - {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f0ea5d0f693cb247a073d21a4123bdf4172e470e6d163c12b74cbb1536cf38"}, - {file = "scikit_learn-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:87440e2e188c87db80ea4023440923dccbd56fbc2d557b18ced00fef79da0727"}, - {file = "scikit_learn-1.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45dee87ac5309bb82e3ea633955030df9bbcb8d2cdb30383c6cd483691c546cc"}, - {file = "scikit_learn-1.4.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1d0b25d9c651fd050555aadd57431b53d4cf664e749069da77f3d52c5ad14b3b"}, - {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0203c368058ab92efc6168a1507d388d41469c873e96ec220ca8e74079bf62e"}, - {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c62f2b124848a28fd695db5bc4da019287abf390bfce602ddc8aa1ec186aae"}, - {file = "scikit_learn-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:5cd7b524115499b18b63f0c96f4224eb885564937a0b3477531b2b63ce331904"}, - {file = "scikit_learn-1.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90378e1747949f90c8f385898fff35d73193dfcaec3dd75d6b542f90c4e89755"}, - {file = "scikit_learn-1.4.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ff4effe5a1d4e8fed260a83a163f7dbf4f6087b54528d8880bab1d1377bd78be"}, - {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:671e2f0c3f2c15409dae4f282a3a619601fa824d2c820e5b608d9d775f91780c"}, - {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36d0bc983336bbc1be22f9b686b50c964f593c8a9a913a792442af9bf4f5e68"}, - {file = "scikit_learn-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:d762070980c17ba3e9a4a1e043ba0518ce4c55152032f1af0ca6f39b376b5928"}, - {file = "scikit_learn-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9993d5e78a8148b1d0fdf5b15ed92452af5581734129998c26f481c46586d68"}, - {file = "scikit_learn-1.4.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:426d258fddac674fdf33f3cb2d54d26f49406e2599dbf9a32b4d1696091d4256"}, - {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5460a1a5b043ae5ae4596b3126a4ec33ccba1b51e7ca2c5d36dac2169f62ab1d"}, - {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d64ef6cb8c093d883e5a36c4766548d974898d378e395ba41a806d0e824db8"}, - {file = "scikit_learn-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:c97a50b05c194be9146d61fe87dbf8eac62b203d9e87a3ccc6ae9aed2dfaf361"}, -] - -[package.dependencies] -joblib = ">=1.2.0" -numpy = ">=1.19.5" -scipy = ">=1.6.0" -threadpoolctl = ">=2.0.0" - -[package.extras] -benchmark = ["matplotlib (>=3.3.4)", "memory-profiler (>=0.57.0)", "pandas (>=1.1.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.15.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=23.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.19.12)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.17.2)"] +developer = ["ipython", "pre-commit", "tomli"] +docs = ["PyWavelets (>=1.1.1)", "dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.6)", "myst-parser", "numpydoc (>=1.7)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.15.2)", "pytest-doctestplus", "pytest-runner", "scikit-learn (>=1.1)", "seaborn (>=0.11)", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-gallery (>=0.14)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] +optional = ["PyWavelets (>=1.1.1)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.6)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=1.1)"] +test = ["asv", "numpydoc (>=1.7)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-doctestplus", "pytest-faulthandler", "pytest-localserver"] [[package]] name = "scipy" -version = "1.13.0" +version = "1.13.1" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"}, - {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"}, - {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"}, - {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"}, - {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"}, - {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"}, - {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"}, - {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"}, - {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"}, - {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"}, - {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"}, - {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"}, - {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"}, - {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"}, - {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"}, - {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"}, - {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"}, - {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"}, - {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"}, - {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"}, - {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"}, - {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"}, - {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"}, - {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"}, - {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, + {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, + {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, + {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, + {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, + {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, + {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, + {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, + {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, + {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, ] [package.dependencies] @@ -5295,64 +4350,6 @@ nativelib = ["pyobjc-framework-Cocoa", "pywin32"] objc = ["pyobjc-framework-Cocoa"] win32 = ["pywin32"] -[[package]] -name = "setuptools" -version = "69.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "silx" -version = "2.0.1" -description = "Software library for X-ray data analysis" -optional = false -python-versions = ">=3.7" -files = [ - {file = "silx-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f21a62239fe00a6af42fdbbc9f85929987d3782379fdabc394ea43d5c05b0a1"}, - {file = "silx-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6c652632c2a91d6aed43ebd2f51b4fa0fba8b5da199ca4e8f0cdb7c4e14ae28"}, - {file = "silx-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bc7df21227df235febe0d694edb85b350504b3ed0f51faa8d98dd3a63e8b6fe"}, - {file = "silx-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:6caa4b2be7ad1d4441b1d4761e02701de1e0e138578aa095b9c126a09527fa5f"}, - {file = "silx-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f007fdf949c5ee4da1916c72198c93c0753c20b2d0147d2f76137f8a453a66a4"}, - {file = "silx-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e26b20060ec3c9fce640162cb1b77c398833cecd56890968d3f1afcf4511ca06"}, - {file = "silx-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7e2ac841e2815550989fe80a778f0c605e9e138dfeca32831149b55a9364301"}, - {file = "silx-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2c1d07372c77cae7de931bf33a5ee3d42cb94caac8234dc09c19cd634c9c2526"}, - {file = "silx-2.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2b5455c2db07e6e8fc33eaebba53bef0b873141a321ccf22629b9972926e7f6f"}, - {file = "silx-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26a43fbeaf76ffe951201268cb0b9168e0c6328acf04b5d8aa04b2b8bd9bc4f"}, - {file = "silx-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b3ec7a6db0c27f5c5835e2f7c66b33a32fae465409baa4680d1a475326428de"}, - {file = "silx-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:30f21e8821f9ef2adbb7132dccc43294af3fce9bf34d98ab68f680920439dc0b"}, - {file = "silx-2.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1ac68e8a3ece1d0140052e1de85bb356634ba14ff286d6d0d90a5a87ee17e2b1"}, - {file = "silx-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91a182e7ac1991cb928cb01f724281a296cc2055e6cd194f583f03d65874d169"}, - {file = "silx-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecbac5e94dcf056040769220c06f4b373c20cc12898dcf80fa44716ab5ad2049"}, - {file = "silx-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:9be8049daf752e4a84ab697aaee06565d5857ae09b4bee9591e923e7308dd6bc"}, - {file = "silx-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:992365b89fe23f164291b46d867a456ce524ebf4bfe90386341b8a32db747890"}, - {file = "silx-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:283b9023ab21b701a858d5f365e5eb575ebdfe08ff6b0441256b34b42e6d5892"}, - {file = "silx-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99c47e61e684f2891828e56c37c329299276b57c58835519d5a0466804569bc1"}, - {file = "silx-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32a6973ae46e5213c488f48475e44b6d97a7ee302290b93fe8ba671eee3693ee"}, - {file = "silx-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8e81590a4189519ff32b99091f142b5657d896c3ec376a92816dd8c8b8f7ce21"}, - {file = "silx-2.0.1.tar.gz", hash = "sha256:dcb7cfee6022fe851348b64bf51aa4d3c19d1a62f60f80507e8b5bb0b36cc1a7"}, -] - -[package.dependencies] -fabio = ">=0.9" -h5py = "*" -numpy = ">=1.21.6,<2" -packaging = "*" - -[package.extras] -doc = ["nbsphinx", "pandoc", "pillow", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinx-panels"] -full = ["Mako", "Pillow", "PyOpenGL", "PyQt5", "bitshuffle", "hdf5plugin", "matplotlib (>=3.1.0)", "pyopencl", "python-dateutil", "qtconsole", "scipy"] -test = ["bitshuffle", "pytest", "pytest-mock", "pytest-xvfb"] - [[package]] name = "six" version = "1.16.0" @@ -5364,17 +4361,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "smmap" -version = "5.0.1" -description = "A pure Python implementation of a sliding window memory map manager" -optional = false -python-versions = ">=3.7" -files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -5408,44 +4394,22 @@ files = [ {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] -[[package]] -name = "sparse" -version = "0.15.1" -description = "Sparse n-dimensional arrays for the PyData ecosystem" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sparse-0.15.1-py2.py3-none-any.whl", hash = "sha256:6d5a19350b0714a1425b653a67df44be330d24e86f21c09f5ad6bb4518c2a18c"}, - {file = "sparse-0.15.1.tar.gz", hash = "sha256:973adcb88a8db8e3d8047953331e26d3f64a5657f9b46a6b859c47663c3eef99"}, -] - -[package.dependencies] -numba = ">=0.49" -numpy = ">=1.17" -scipy = ">=0.19" - -[package.extras] -all = ["matrepr", "sparse[docs,tox]"] -docs = ["sphinx", "sphinx-rtd-theme"] -tests = ["dask[array]", "pre-commit", "pytest (>=3.5)", "pytest-cov"] -tox = ["sparse[tests]", "tox"] - [[package]] name = "sphinx" -version = "7.2.6" +version = "7.3.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ - {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, - {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" @@ -5459,30 +4423,31 @@ sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autodoc-typehints" -version = "2.0.1" +version = "2.2.1" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, - {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, + {file = "sphinx_autodoc_typehints-2.2.1-py3-none-any.whl", hash = "sha256:ac37852861c58a5ca95be13d5a0f49f3661b5341eaf7de8531842135600aeb90"}, + {file = "sphinx_autodoc_typehints-2.2.1.tar.gz", hash = "sha256:26a81e6444c9b82a952519a3b7c52e45f14a0f81c91cfc7063cfcf2ca109d161"}, ] [package.dependencies] -sphinx = ">=7.1.2" +sphinx = ">=7.3.5" [package.extras] docs = ["furo (>=2024.1.29)"] numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] [[package]] name = "sphinx-rtd-theme" @@ -5653,56 +4618,6 @@ scipy = "*" sphinx = "*" sphinx-rtd-theme = "*" -[[package]] -name = "sympy" -version = "1.12" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, - {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, -] - -[package.dependencies] -mpmath = ">=0.19" - -[[package]] -name = "tables" -version = "3.9.2" -description = "Hierarchical datasets for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "tables-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a4e71fc9d2a3a0cacce4994afd47cd5f4797093ff9cee2cc7dc87e51f308107"}, - {file = "tables-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbea426ce9bdd60cda435a265823b31d18f2b36e9045fb2d565679825a7aa46"}, - {file = "tables-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e756c272bb111d016fec1d03a60095403a8fb42a5fbaf5f317dcf6e3b9d8e92e"}, - {file = "tables-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:eea41cb32dd22b30d6f3dd4e113f6d693384d301c89f3c4b4712f90c9c955875"}, - {file = "tables-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d71913fb8147dc6132595b94fc82f88f6c2436a3b5c57aadfe26c680f96aa387"}, - {file = "tables-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d6bbc477d038a17c5062ab6ccd94c8b1fa365cf017b9a2ad6c2dff1a07abb2b"}, - {file = "tables-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e67c71070b871fade3694a4c764504e03836bb1843321766cf2e40b7d280e84"}, - {file = "tables-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ab9291ff4d243e7966b6706a2675b83138bd9bbe82721d695b78971660d59632"}, - {file = "tables-3.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c14dc879b041cf53be1afe9e5ed581e1aeacdcee9e2e1ee79110dc96a4c8d97c"}, - {file = "tables-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2848fb3dce30a7b83fa099d026a91d7b10ad48afae04fa10f974f1da3f1e2bbf"}, - {file = "tables-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b131c9b4e003816a45e2efe5c5c797d01d8308cac4aee72597a15837cedb605c"}, - {file = "tables-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:c6304d321452fd56865e5c309e38373011b0f0f6c714786c5660613ceb623acb"}, - {file = "tables-3.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c52087ed8b90a5f6ba87f0adcd1c433e5f5db7c7ca5984b08ff45f2247635f7d"}, - {file = "tables-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:164b945d0cb731c7232775fd3657f150bcf05413928b86033b023a1dc8dbeb05"}, - {file = "tables-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a068d4ad08d5a6b2ad457f60ac6676efdab9e29459e776e433d5537a46e62e41"}, - {file = "tables-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:bca5a6bf162a84a6ef74ca4017b28c59c1526cffdbd93ce94c98ff8f9593f1d5"}, - {file = "tables-3.9.2.tar.gz", hash = "sha256:d470263c2e50c4b7c8635a0d99ac1ff2f9e704c24d71e5fa33c4529e7d0ad9c3"}, -] - -[package.dependencies] -blosc2 = ">=2.3.0" -numexpr = ">=2.6.2" -numpy = ">=1.19.0" -packaging = "*" -py-cpuinfo = "*" - -[package.extras] -doc = ["ipython", "numpydoc", "sphinx (>=1.1,<6)", "sphinx-rtd-theme"] - [[package]] name = "terminado" version = "0.18.1" @@ -5726,24 +4641,24 @@ typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] [[package]] name = "threadpoolctl" -version = "3.4.0" +version = "3.5.0" description = "threadpoolctl" optional = false python-versions = ">=3.8" files = [ - {file = "threadpoolctl-3.4.0-py3-none-any.whl", hash = "sha256:8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262"}, - {file = "threadpoolctl-3.4.0.tar.gz", hash = "sha256:f11b491a03661d6dd7ef692dd422ab34185d982466c49c8f98c8f716b5c93196"}, + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, ] [[package]] name = "tifffile" -version = "2024.2.12" +version = "2024.6.18" description = "Read and write TIFF files" optional = false python-versions = ">=3.9" files = [ - {file = "tifffile-2024.2.12-py3-none-any.whl", hash = "sha256:870998f82fbc94ff7c3528884c1b0ae54863504ff51dbebea431ac3fa8fb7c21"}, - {file = "tifffile-2024.2.12.tar.gz", hash = "sha256:4920a3ec8e8e003e673d3c6531863c99eedd570d1b8b7e141c072ed78ff8030d"}, + {file = "tifffile-2024.6.18-py3-none-any.whl", hash = "sha256:67299c0445fc47463bbc71f3cb4676da2ab0242b0c6c6542a0680801b4b97d8a"}, + {file = "tifffile-2024.6.18.tar.gz", hash = "sha256:57e0d2a034bcb6287ea3155d8716508dfac86443a257f6502b57ee7f8a33b3b6"}, ] [package.dependencies] @@ -5754,13 +4669,13 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib" [[package]] name = "tinycss2" -version = "1.2.1" +version = "1.3.0" description = "A tiny CSS parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, + {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, + {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, ] [package.dependencies] @@ -5768,7 +4683,7 @@ webencodings = ">=0.4" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] +test = ["pytest", "ruff"] [[package]] name = "tomli" @@ -5783,13 +4698,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.12.5" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, ] [[package]] @@ -5805,33 +4720,33 @@ files = [ [[package]] name = "tornado" -version = "6.4" +version = "6.4.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, - {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, - {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, - {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, - {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, ] [[package]] name = "tqdm" -version = "4.66.2" +version = "4.66.4" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, - {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, ] [package.dependencies] @@ -5845,103 +4760,18 @@ telegram = ["requests"] [[package]] name = "traitlets" -version = "5.14.2" +version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, - {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "traits" -version = "6.4.3" -description = "Observable typed attributes for Python classes" -optional = false -python-versions = ">=3.7" -files = [ - {file = "traits-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64bfbd0fc4c8fa25eb55d6d46a8f98992852acaf904618879dbbba87a09289aa"}, - {file = "traits-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1e97f8a45c161715dfb69d5248cb54210057b5bd649caf08041465374dadbc1"}, - {file = "traits-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb126a6485edab740b2f238e9b02c830f7c8f7c1112609b4b5d280aef3e9421"}, - {file = "traits-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35f0bd177409697a33e95d5e5066670db122f0b5451e7a0ddffc9752d2bf3f48"}, - {file = "traits-6.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57109bca87c1e9ec125332a758c95cdf940540efc4c53a30df8d1dfa119472b9"}, - {file = "traits-6.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46cc41d2c65b374fbc600f39fe4bd68bbd01bfbdd6629c29e39686a854d4cfd0"}, - {file = "traits-6.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2faa1467ac1da29f295a90ef0867474be79edb5aea133811c0e2403ac645adf9"}, - {file = "traits-6.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:053d7c176cc83b228d497cf96171792fb96b91a342bb9666071d513b4e064e66"}, - {file = "traits-6.4.3-cp310-cp310-win32.whl", hash = "sha256:371c2b24daa7534206c8353d2fc62663b7068c25eb5f9a2a865e115e940abedf"}, - {file = "traits-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:fa8bc65a24dc3638f94f4fbc7d60c1838bb4e569c73317d37ab57fb3f23abb08"}, - {file = "traits-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b098ecde49f78bc2587f43880d1344a8a81c9862244ad7e63d48409a8855b5c"}, - {file = "traits-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8348b7f54f4a4be6c60872c23c5ec00db2322591d12d4eee3f3c7f472585504a"}, - {file = "traits-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f791cca287a428dda5adc833915789f60dea1241e9ec21bde47668538fb01f40"}, - {file = "traits-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5d8eaa97adfb079872e760fab29452506f3cbe03e37a055737dae620d06ad5c"}, - {file = "traits-6.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f622915217b5edfb95bf043a9eb75e7ab7c2f0697b59a92b3481e58c883b1e7"}, - {file = "traits-6.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3ed9ea3e7f830460fe39aeb170816152df87d8e99947034e74f2b58178599253"}, - {file = "traits-6.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:22d17a47dead1d78fcd7c85dc961e646c4b924ee6f0005b5b7020b88aeeee2ad"}, - {file = "traits-6.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:79f937db0b0bd61272867f4a104d86a132cffab6cf8046f590fed800d621d9bd"}, - {file = "traits-6.4.3-cp311-cp311-win32.whl", hash = "sha256:c51635d076b4c2919e7fd7e82198cd7c3d5d0beee2cc4a5c366f2706dee1e465"}, - {file = "traits-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:c0e8ef49bcf8ab4e880009de934ce06a4f3fe47b3a064807eebd0e80798c0ab5"}, - {file = "traits-6.4.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dba809d945d599980694b69c5d5756115959a3899afa9354f0994eebdc843e14"}, - {file = "traits-6.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e1600875fc3b1cd0502ce0d4317479e0ae0b91e4fa6f14fff1cc525eb1ad088e"}, - {file = "traits-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92add65d02b4d9ebd6b607948eff84c3a4dfaa642335e3d63c5d8c5a52f08e98"}, - {file = "traits-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13181f5433dc2d2fc2d9ee3f7ee3d0ede734c4eb53311ab9710500e7c45ae2c2"}, - {file = "traits-6.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2203ab21df7fd58d0eefb26c56501654ababcafe5e9299bac83dc1ce559be2f4"}, - {file = "traits-6.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44a663117ebd197dbba4a9222ac0ecbb7a6bff7cf97e051924e987685025edd9"}, - {file = "traits-6.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:83f531a45d681bbe133953c5d5a5a4b572d06239672b3346fb927706bee0c9a9"}, - {file = "traits-6.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7b1f5763ae1e6b1cab0ce7c82f7da3a642170666fba32dd643181f564e576d4d"}, - {file = "traits-6.4.3-cp312-cp312-win32.whl", hash = "sha256:f1253043fb8f034c4342b78e56490a1f2386ed6a645cf5e33e3aa428b4b680b6"}, - {file = "traits-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:a01968bff6b13a0dc64842caf598ffc628bd973610b309d14b50fa92ce493a56"}, - {file = "traits-6.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:48394bbc474c6e160897d339791750e75aec638c426c0e930f54e308a43ada47"}, - {file = "traits-6.4.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b6777907777c595103e1c11c423eea907e895921b763eab0c8a213cc81d2224"}, - {file = "traits-6.4.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e69c544a48223f5bb662fbf2b1810d90be6e1bc45245345cbddd446658b4b3"}, - {file = "traits-6.4.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2ca6513b6449877dc2f7e9c10aab7b2b6fca47d95920139dbefd1098eb4f2f1"}, - {file = "traits-6.4.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e53c14dd203d7527eeab52e3ea62dc360df634b1814e79c1015470d1ea254dd"}, - {file = "traits-6.4.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:01ae3e7567d4b06dcabb2d2c32e5fced42f05ebb1709cf41e7f4d816f3ce3eb0"}, - {file = "traits-6.4.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4b7d5297b3c7541fda86a244f873aa29a9a09e7fd281b63f44ba3b864de6380"}, - {file = "traits-6.4.3-cp37-cp37m-win32.whl", hash = "sha256:2b09fad29dd5b153c510f699e65da87f04c5c7d25d03d2376b9f802e52c35219"}, - {file = "traits-6.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6b75ffe02d9b2f870b647df9a78740f65d9d599594060ab0985dcad3391d2b28"}, - {file = "traits-6.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4755a4ee9ea6bc9287e03e4ff5f0b80d12f62c10c1cb1b9fd4c8648650a485c8"}, - {file = "traits-6.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:370b892835e057e9353ea26a31127dccc427f72eb8f400200bf4d0ed4fa4293a"}, - {file = "traits-6.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d3f0887f6261f7595a80a97a995a229109e2902b66bf370caa4468ffe5b7f56"}, - {file = "traits-6.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:309131c6633eb80a6a73b90d0aef83dfbb8c81c81452c81dda0f80dc38df7cf6"}, - {file = "traits-6.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:996bdc5ffcb2fc1edc08ecfa86cb42fe49b80d1b71abcd8379c852b87c1d1ec6"}, - {file = "traits-6.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6bda9dd716ab4ee296a24d3948dbc84beb2b06b2aadf3f56fe293750a234f40"}, - {file = "traits-6.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f950691085c32ac30d66d90e1347fd7971ffc118c1ba36c39aca7d3826f61b3"}, - {file = "traits-6.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dba974e6277353f01d5cd780b2efbf22142fc81e7907da3a50c6b49ed67f10bc"}, - {file = "traits-6.4.3-cp38-cp38-win32.whl", hash = "sha256:c7b8983528582c3b22f04b341470be512ca43d8036c96acd6a038fdb0ddd9769"}, - {file = "traits-6.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:cdf92353fd3522e409b245c025b424294264724ce6fa6c12b47fab56e66d915d"}, - {file = "traits-6.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab854a855947a544f7b6644086499d13e7a8f0c23dda9dbfc2f50b4439848303"}, - {file = "traits-6.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1951ef5a5702f80a83b825747969bd910eaa7f093658d092342eae8004617580"}, - {file = "traits-6.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd8467a47c4369e85efeb86a48d5e4540209b0f58f2a66ddad65d8245cddc86"}, - {file = "traits-6.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220b8faac11ae71045a9d0e65ef32e9298fc545f108605a149a81f38fdbfec9d"}, - {file = "traits-6.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51589af37bac46d8af91593a0f44116d416db830a59491f603e982fe2d96bed5"}, - {file = "traits-6.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:109f835eafe8106a76245b9dc6455d52cd315d60965987c3d27061f9f4a6fc22"}, - {file = "traits-6.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d3b5977372d983c39856ebdbd8389b82458b0907c1fc3a801f831ccb1de740a1"}, - {file = "traits-6.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79fe4de34f6c5cb9aa5ba21dfbbf4ba232bd1b74882a49c3fe10883806ebf422"}, - {file = "traits-6.4.3-cp39-cp39-win32.whl", hash = "sha256:8af9f2bafea38a9747798a90b61ac5f806cc6d930889d993c899bb0862e0d3e1"}, - {file = "traits-6.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:723bb5c8b0f7f1125092f6ed8c1fc21d9673d64b6ed33b30f1933ab5de357f7d"}, - {file = "traits-6.4.3.tar.gz", hash = "sha256:a9bbfd9e0c08b7de07e86ef64e69cb96a29c2105a43bf832cd8b162fa1e22f44"}, -] - -[package.extras] -docs = ["Sphinx", "enthought-sphinx-theme", "pygments (<2.15)", "sphinx-copybutton"] -examples = ["numpy", "pillow"] -test = ["Cython", "PySide6", "Sphinx", "flake8", "flake8-ets", "mypy", "numpy", "pyface", "pygments (<2.15)", "setuptools", "traitsui"] - -[[package]] -name = "transforms3d" -version = "0.4.1" -description = "Functions for 3D coordinate transformations" -optional = false -python-versions = ">=3.6" -files = [ - {file = "transforms3d-0.4.1-py3-none-any.whl", hash = "sha256:aea08776c1c915c8b424418994202aced8e46301c375ce63423d14f1d0045aa7"}, - {file = "transforms3d-0.4.1.tar.gz", hash = "sha256:31c755266a0b0a222488b8d039f6f325cf486c52728c03e307ce047b2fad1179"}, -] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "types-python-dateutil" @@ -5967,13 +4797,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.20240406" +version = "2.32.0.20240602" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, - {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, + {file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"}, + {file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"}, ] [package.dependencies] @@ -5981,13 +4811,13 @@ urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -6001,43 +4831,22 @@ files = [ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] -[[package]] -name = "tzlocal" -version = "4.3" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"}, - {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"}, -] - -[package.dependencies] -pytz-deprecation-shim = "*" -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - [[package]] name = "uncertainties" -version = "3.1.7" -description = "Transparent calculations with uncertainties on the quantities involved (aka error propagation); fast calculation of derivatives" +version = "3.2.1" +description = "calculations with values with uncertainties, error propagation" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "uncertainties-3.1.7-py2.py3-none-any.whl", hash = "sha256:4040ec64d298215531922a68fa1506dc6b1cb86cd7cca8eca848fcfe0f987151"}, - {file = "uncertainties-3.1.7.tar.gz", hash = "sha256:80111e0839f239c5b233cb4772017b483a0b7a1573a581b92ab7746a35e6faab"}, + {file = "uncertainties-3.2.1-py3-none-any.whl", hash = "sha256:80dea7f0c2fe37c9de6893b2352311b5f332be60060cbd6387f88050f7ec345d"}, + {file = "uncertainties-3.2.1.tar.gz", hash = "sha256:b05417b58bdef236c20e711fb2fee18e4db7348a92edcec01318b32aab34925e"}, ] -[package.dependencies] -future = "*" - [package.extras] -all = ["nose", "numpy", "sphinx"] -docs = ["sphinx"] -optional = ["numpy"] -tests = ["nose", "numpy"] +all = ["uncertainties[arrays,doc,test]"] +arrays = ["numpy"] +doc = ["python-docs-theme", "sphinx", "sphinx-copybutton"] +test = ["pytest", "pytest-cov"] [[package]] name = "uri-template" @@ -6055,13 +4864,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -6070,6 +4879,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "virtualenv" +version = "20.26.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -6083,18 +4912,18 @@ files = [ [[package]] name = "webcolors" -version = "1.13" +version = "24.6.0" description = "A library for working with the color formats defined by HTML and CSS." optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, - {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, + {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, + {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, ] [package.extras] docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["pytest", "pytest-cov"] +tests = ["coverage[toml]"] [[package]] name = "webencodings" @@ -6109,17 +4938,17 @@ files = [ [[package]] name = "websocket-client" -version = "1.7.0" +version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, - {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, ] [package.extras] -docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] @@ -6139,37 +4968,37 @@ notebook = ">=4.4.1" [[package]] name = "xarray" -version = "2024.3.0" +version = "2024.6.0" description = "N-D labeled arrays and datasets in Python" optional = false python-versions = ">=3.9" files = [ - {file = "xarray-2024.3.0-py3-none-any.whl", hash = "sha256:ca2bc4da2bf2e7879e15862a7a7c3fc76ad19f6a08931d030220cef39a29118d"}, - {file = "xarray-2024.3.0.tar.gz", hash = "sha256:5c1db19efdde61db7faedad8fc944f4e29698fb6fbd578d352668b63598bd1d8"}, + {file = "xarray-2024.6.0-py3-none-any.whl", hash = "sha256:721a7394e8ec3d592b2d8ebe21eed074ac077dc1bb1bd777ce00e41700b4866c"}, + {file = "xarray-2024.6.0.tar.gz", hash = "sha256:0b91e0bc4dc0296947947640fe31ec6e867ce258d2f7cbc10bedf4a6d68340c7"}, ] [package.dependencies] numpy = ">=1.23" -packaging = ">=22" -pandas = ">=1.5" +packaging = ">=23.1" +pandas = ">=2.0" [package.extras] accel = ["bottleneck", "flox", "numbagg", "opt-einsum", "scipy"] complete = ["xarray[accel,dev,io,parallel,viz]"] -dev = ["hypothesis", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "xarray[complete]"] +dev = ["hypothesis", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "xarray[complete]"] io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] parallel = ["dask[complete]"] viz = ["matplotlib", "nc-time-axis", "seaborn"] [[package]] name = "xyzservices" -version = "2024.4.0" +version = "2024.6.0" description = "Source of XYZ tiles providers" optional = false python-versions = ">=3.8" files = [ - {file = "xyzservices-2024.4.0-py3-none-any.whl", hash = "sha256:b83e48c5b776c9969fffcfff57b03d02b1b1cd6607a9d9c4e7f568b01ef47f4c"}, - {file = "xyzservices-2024.4.0.tar.gz", hash = "sha256:6a04f11487a6fb77d92a98984cd107fbd9157fd5e65f929add9c3d6e604ee88c"}, + {file = "xyzservices-2024.6.0-py3-none-any.whl", hash = "sha256:fecb2508f0f2b71c819aecf5df2c03cef001c56a4b49302e640f3b34710d25e4"}, + {file = "xyzservices-2024.6.0.tar.gz", hash = "sha256:58c1bdab4257d2551b9ef91cd48571f77b7c4d2bc45bf5e3c05ac97b3a4d7282"}, ] [[package]] @@ -6274,51 +5103,20 @@ y-py = ">=0.6.0,<0.7.0" [package.extras] test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"] -[[package]] -name = "zarr" -version = "2.17.2" -description = "An implementation of chunked, compressed, N-dimensional arrays for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zarr-2.17.2-py3-none-any.whl", hash = "sha256:70d7cc07c24280c380ef80644151d136b7503b0d83c9f214e8000ddc0f57f69b"}, - {file = "zarr-2.17.2.tar.gz", hash = "sha256:2cbaa6cb4e342d45152d4a7a4b2013c337fcd3a8e7bc98253560180de60552ce"}, -] - -[package.dependencies] -asciitree = "*" -fasteners = {version = "*", markers = "sys_platform != \"emscripten\""} -numcodecs = ">=0.10.0" -numpy = ">=1.23" - -[package.extras] -docs = ["numcodecs[msgpack]", "numpydoc", "pydata-sphinx-theme", "sphinx", "sphinx-automodapi", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] -jupyter = ["ipytree (>=0.2.2)", "ipywidgets (>=8.0.0)", "notebook"] - -[[package]] -name = "zipfile37" -version = "0.1.3" -description = "Read and write ZIP files - backport of the zipfile module from Python 3.7" -optional = false -python-versions = "*" -files = [ - {file = "zipfile37-0.1.3.tar.gz", hash = "sha256:75daba739cbe2c7cff2f27debbf55eb4ea325c1f2111b5d60911f48c846e26be"}, -] - [[package]] name = "zipp" -version = "3.18.1" +version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] @@ -6326,4 +5124,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "afc0fbd0c0231c8c0ecc67077a3cc55c3672cf201f1cbc39c81a0d77ded55049" +content-hash = "bb98b14e71e5f15860f0e61aa08637beb3e1a08973148f7ae000c2875f87093e" diff --git a/pyproject.toml b/pyproject.toml index 94a92052..a7185047 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9, <3.13" bokeh = ">=2.4.2" -dask = {extras = ["dataframe"], version = ">=2021.12.0"} +dask = {version = ">=2021.12.0, <2024.3.0"} fastdtw = ">=0.3.4" fastparquet = ">=0.8.0" h5py = ">=3.6.0" @@ -44,7 +44,6 @@ ipykernel = {version = ">=6.9.1", optional = true} jupyterlab = {version = "^3.4.0", optional = true} jupyterlab-h5web = {version = "^8.0.0", extras = ["full"]} - [tool.poetry.extras] notebook = ["jupyter", "ipykernel", "jupyterlab", "jupyterlab-h5web"] @@ -59,7 +58,7 @@ types-pyyaml = ">=6.0.12.12" types-requests = ">=2.31.0.9" pyfakefs = ">=5.3.0" requests-mock = "^1.11.0" - +pre-commit = ">=3.0.0" [tool.poetry.group.docs] optional = true diff --git a/tests/test_dfops.py b/tests/test_dfops.py index 7ae888f4..2ce894dc 100644 --- a/tests/test_dfops.py +++ b/tests/test_dfops.py @@ -343,7 +343,6 @@ def test_offset_by_other_columns_functionality() -> None: offset_columns=["off1"], weights=[1], ) - res = res.compute() expected: List[Any] = [11, 22, 33, 44, 55, 66] np.testing.assert_allclose(res["target"].values, expected) @@ -353,7 +352,6 @@ def test_offset_by_other_columns_functionality() -> None: offset_columns=["off1", "off2"], weights=[1, -1], ) - res = res.compute() expected = [10.9, 21.8, 32.7, 43.6, 54.5, 65.4] np.testing.assert_allclose(res["target"].values, expected) @@ -364,7 +362,6 @@ def test_offset_by_other_columns_functionality() -> None: weights=[1], preserve_mean=True, ) - res = res.compute() expected = [9.75, 19.85, 29.95, 40.05, 50.15, 60.25] np.testing.assert_allclose(res["target"].values, expected) @@ -375,7 +372,6 @@ def test_offset_by_other_columns_functionality() -> None: weights=[1], reductions="mean", ) - res = res.compute() expected = [20, 30, 40, 50, 60, 70] np.testing.assert_allclose(res["target"].values, expected) From 502f4d75f823b87f05535ad9382867e8f11afa71 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 21:27:24 +0200 Subject: [PATCH 078/300] fruther restrict dask due to never finishing tests --- poetry.lock | 238 ++++++++----------------------------------------- pyproject.toml | 2 +- 2 files changed, 39 insertions(+), 201 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6c494a47..55f8988c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -778,118 +778,6 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] -[[package]] -name = "cramjam" -version = "2.8.3" -description = "Thin Python bindings to de/compression algorithms in Rust" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cramjam-2.8.3-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8c8aa6d08c135ae7f0da01e6559a332c5d8fe4989a594db401040e385d04dffd"}, - {file = "cramjam-2.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bd8c601fe8717e52517a2f2eef78217086acf449627bfdda97e3f53fd79c92af"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dac42b2b4c3950e7eda9b5551e0e904784ed0c0428accc29171c230fb919ec72"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab8146faa5d8c52edf23724843c36469fc32ff2c4a174eba72f4da6de5016688"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb5f4d061e9abdc6663551446c332a58c101efb31fd1746229872600274c2b20"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d1ac94e00c64258330105473c641441db02b4dc3e9e9f2963d204e53ed93025"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ed658f36a2bf667d5b8c7c6690103ad99f81cc62a1b64891b69298447329d4b"}, - {file = "cramjam-2.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f6303c8cc583dfe5054cf84717674f75b18bca4ae8e576dc863958d5494dc4b"}, - {file = "cramjam-2.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04b31d427a8902e5c2eec4b8f29873de7a3ade202e3d68e7f2354b9f0aa00bc7"}, - {file = "cramjam-2.8.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:9728861bc0390681824961778b36f7f0b95039e8b90d46f1b67f51232f1ee159"}, - {file = "cramjam-2.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87e26e3e1d5fed1cac5b41be648d0daf0793f94cf4a7aebefce1f4f6656e2d21"}, - {file = "cramjam-2.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1d2d39c2193a77c5e5b327944f90e6ecf2caa1b55e7176cc83d80706ea15de"}, - {file = "cramjam-2.8.3-cp310-none-win32.whl", hash = "sha256:6721edd8f911ad84db83ee4902b7579fc01c55849062f3f1f4171b58fccf98eb"}, - {file = "cramjam-2.8.3-cp310-none-win_amd64.whl", hash = "sha256:4f7c16d358df366e308137411125a2bb50d1b19924fced3a390898fa8c9a074d"}, - {file = "cramjam-2.8.3-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:24c2b426dd8fafb894f93a88f42e2827e14199d66836cb100582037e5371c724"}, - {file = "cramjam-2.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:007aa9444cb27b8691baae73ca907133cd939987438f874774011b4c740732dd"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:29987b54e31efed66738e8f236c597c4c9a91ec9d57bcb74307712e07505b4bb"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65bfd41aa92c0025f32ba09214b48e9367a81122586b2617439b4327c4bd179c"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7337bd8218bd8508f35904274a38cce843a237fe6e23104238bbeb2f337107ed"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:269f94d2efe6b6a97624782cd3b541e60535dd5874f4a8d5d0ba66ef59424ae3"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bec9ca5431c32ba94996b7c1c56695b37d48713b97ee1d2a456f4046f009e82f"}, - {file = "cramjam-2.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cb64a97e625ca029b55e37769b8c354e64cbea042c75471915dc385935d30ed"}, - {file = "cramjam-2.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c28830ecf76501356d678dac4f37563554ec1c651a53a990cdf595f7ed75c651"}, - {file = "cramjam-2.8.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35647a0e37a4dfec85a44c7966ae476b7db0e6cd65d91c08f1fb3007ed774d92"}, - {file = "cramjam-2.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e954599c6369f429a868852eff453b894d88866acba439b65131ea93f5400b47"}, - {file = "cramjam-2.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:86e238b6de79e045f5197df2c9dfaf8d10b37a6517ff4ffc4775fe5a3cf4d4a4"}, - {file = "cramjam-2.8.3-cp311-none-win32.whl", hash = "sha256:fe6434d3ee0899bc9396801d1abbc5d1fe77662bd3d1f1c1573fac6708459138"}, - {file = "cramjam-2.8.3-cp311-none-win_amd64.whl", hash = "sha256:e8ec1d4f27eb9d0412f0c567e7ffd14fbeb2b318a1ac394d5de4047c431fe94c"}, - {file = "cramjam-2.8.3-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:24990be4010b2185dcecc67133cd727657036e7b132d7de598148f5b1eb8e452"}, - {file = "cramjam-2.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:572cb9a8dc5a189691d6e03a9bf9b4305fd9a9f36bb0f9fde55fc36837c2e6b3"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9efe6915aa7ef176f3a7f42a4e46504573215953331b139abefd20d07d8aba82"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe84440100e7045190da7f80219be9989b0b6db6acadb3ae9cfe0935d93ebf8c"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00524bb23f4abb3a3bfff08aa32b9274843170c5b43855807e0f59670e2ac98c"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab67f29094165f0771acad8dd16e840259cfedcc94067af229530496dbf1a24c"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be6fb5dd5bf1c89c717a73a1057505959f35c08e0e97a76d4cc6391b90d2263b"}, - {file = "cramjam-2.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93b42d22bf3e17290c5e4cf58e715a419330bb5255c35933c14db82ecf3872c"}, - {file = "cramjam-2.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:afa065bab70e27565695441f69f493af3d379b8723030f2c3d2547d2e312a4be"}, - {file = "cramjam-2.8.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:832224f52fa1e601e0ab678dba9bdfde3686fc4cd1a9f2ed4748f29eaf1cb553"}, - {file = "cramjam-2.8.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:962b7106287bcc463150766b5b8c69f32dcc69713a8dbce00e0ca6936f95c55b"}, - {file = "cramjam-2.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2be92c6f0bcffaf8ea6a8164fe0388a188fec2fa9eff1828e8b64dc3a83740f9"}, - {file = "cramjam-2.8.3-cp312-none-win32.whl", hash = "sha256:080f3eb7b648f5ba9d35084d8dddc68246a8f365df239792f6712908f0aa568e"}, - {file = "cramjam-2.8.3-cp312-none-win_amd64.whl", hash = "sha256:c14728e3360cd212d5b606ca703c3bd1c8912efcdbc1aa032c81c2882509ebd5"}, - {file = "cramjam-2.8.3-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:c7e8329cde48740df8d332dade2f52b74612b8ea86005341c99bb192c82a5ce7"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77346ac669f5445d14b74476a4e8f3a259fd22681bd73790e92b8956d7e225fc"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274878883e7fadf95a6b5bc58f9c1dd39fef2c31d68e18a0fb8594226457fba7"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7871e1fd3ee8ca16799ba22d49fc1e52e78976fa8c659be41630eeb2914475a7"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:345a952c5d4b922830efaa67dc0b42d21e18c182c1a1bda6d20bb78235f31d6f"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb5d7739e2bc573ade12327ef7717b1ac5876c62938fab20eb54d762da23cae2"}, - {file = "cramjam-2.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440a18fd4ae42e06dbbd7aee91d8248b61da9fef7610ffbd553d1ba93931394b"}, - {file = "cramjam-2.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:476890974229713fc7b4c16fb050b756ba926c67e4d1200b3e03c5c051e9b552"}, - {file = "cramjam-2.8.3-cp37-cp37m-musllinux_1_1_armv7l.whl", hash = "sha256:771b44e549f90b5532508782e25d1c40b8054dd83d52253d05945fc05836b252"}, - {file = "cramjam-2.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d824fd98364bc946c38ed324a3ec7befba055285aaf2c1ca61894bb7616226e8"}, - {file = "cramjam-2.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2476828dea4089aa3cb9160391f8b36f793ca651afdcba80de1e341373928397"}, - {file = "cramjam-2.8.3-cp37-none-win32.whl", hash = "sha256:4a554bcfd068e831affd64a4f067c7c9b00b359742597c4fdadd18ff673baf30"}, - {file = "cramjam-2.8.3-cp37-none-win_amd64.whl", hash = "sha256:246f1f7d32cac2b64617d2dddba11a82851e73cdcf9d1abb799b08dcd9d2ea49"}, - {file = "cramjam-2.8.3-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc8f24c32124bb47536882c6b941cdb88cc16e4fa64d5bf347cb8dd72a193fc3"}, - {file = "cramjam-2.8.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:28c30078effc100739d3f9b227276a8360c1b32aac65efb4f641630552213548"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef0173fb457f73cf9c2553092419db0eba4d582890db95e542a4d93e11340421"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a1943f2cc0deee037ddcf92beff6049e12d4e6d557f568ddf59fb3b848f2152"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5023a737d8d9cf5d123e6d87d088929c3cfb2aae90e0f584204427f74882150a"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eec7e985f35708c234542721863d82781d0f7f6a71b45e14ce6d2625d4b131d"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b188e750b95172c01defcfcfbba629cad797718b34402ec61b3bc9ff99403599"}, - {file = "cramjam-2.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e2d745cd4d244b7973d15aaebeedb537b980f9d3da80e6dea75ee1a872f9fa"}, - {file = "cramjam-2.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c9d54a4aa475d5e902f2ee518bdaa02f26c089e9f72950d00d1643c090f0deb3"}, - {file = "cramjam-2.8.3-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:19b8c97350c8d65daea26267dd1becb59073569aac2ae5743952d7f48da5d37a"}, - {file = "cramjam-2.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3277fd42399755d6d3730edec4a192174ee64d219e0ffbc90613f15cbabf711f"}, - {file = "cramjam-2.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1fd25201f1278dc6faa2ae35e67b7a5bb352b7fc6ed1ee939637414ca8115863"}, - {file = "cramjam-2.8.3-cp38-none-win32.whl", hash = "sha256:594477faff7f4380fa123cfbcf10ab8ee5af1a28b95750b66931ffafcb11ab5c"}, - {file = "cramjam-2.8.3-cp38-none-win_amd64.whl", hash = "sha256:8ea1dc11538842ff20d9872a17214994f5913cbf3be5594b54aad2422becdf19"}, - {file = "cramjam-2.8.3-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6379b92912f7569e126bd48d10e7087ddd20ea88a939532e3c4a85c2fa05d600"}, - {file = "cramjam-2.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:11d2e9eebc7d202eda0ae09fb56a2cdbeb5a1563e89d2118bf18cf0030f35f77"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d5a0a2fe240c97587df07f3d5e1027673d599b3a6a7a0ab540aea69f09e9ff7a"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba542f07fe3f41475d78626973533539e6cf2d5b6af37923fe6c7e7f0f74b9b2"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1374fe9a4431e546bb4501a16b84875d0bf80fc4e6c8942f0d5608ae48474267"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcf7791e1cedb982ccc873ec9392c6cfb9c714a64ebf1ed4e8310b9cb44655f2"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:990e65c2bf1c155a9ddec5ecabf431cf77596432f697d3c6e0831b5174c51c40"}, - {file = "cramjam-2.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9b244d04cef82872d12c227a2f202f080a454d664c05db351626e6ad4aaa307"}, - {file = "cramjam-2.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:80b088d15866b37851fd53e2b471becc9ec487257dceca1878621072a18e833e"}, - {file = "cramjam-2.8.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f667843e7a8fca208eecfe44e04088242f8ca60d74d4950fac3722043538d700"}, - {file = "cramjam-2.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6f838d06d06709b9ce8b1ceae36aea4e1c7e613365185a91edcbeb5884f5e606"}, - {file = "cramjam-2.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4822eb5fe6839cd3d0439e5431e766ad010b2a388ca9617aa6372b6030897782"}, - {file = "cramjam-2.8.3-cp39-none-win32.whl", hash = "sha256:67e09b42e744efd08b93ac56f6100a859a31617d7146725516f3f2c744149d97"}, - {file = "cramjam-2.8.3-cp39-none-win_amd64.whl", hash = "sha256:11c9d30bc53892c57a3b296756c23659323ab1419a2b4bf22bbafc07b247bb67"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:51e847dcfe74fba379fed2bc2b45f5c2f11c3ece5e9eebcf63f39a9594184588"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07af94191f6a245226dc8a8bc6c94808e382ce9dfcca4bab0e8015fbc7fc3322"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9c45469914099897c47bfc501616fb377f28a865adebf90ea6f3c8ae6dd4e6"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ef29fb916fe74be65d0ab8871ab8d964b0f5eb8028bb84b325be43675a59d6e7"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3850dac9a2f6dcb3249d23f9d505117643b967bdc1c572ed0cc492a48fd69daf"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:e23e323ad28ed3e4e3a24ceffdab0ff235954109a88b536ea7b3b7886bd0a536"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1ba1a8ff855b30b4069a9b45ea9e7f2b5d882c7953bdfccda8d4b275fa7057ce"}, - {file = "cramjam-2.8.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eea606b01b43b91626e3aafd463bd19b6ed739bdb8b2b309e5d7ff72afc0e89d"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:97c706c520c3f8b0184278cc86187528458350216c6e4fa85d3f16bcad0d365d"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d08f1bab949ffd6dd6f25a89e4f7062d147aeea9c067e4dd155bdb190e5a519"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba1e45074757ab0482ac544e60613b6b8658100ac9985c91868a4598cdfb63ba"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a2fededed05a042f093dbf1b11d69afb1874a2c9197fcf1d58c142ba9111db5a"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:fc0c6eb8185c68f79a25bb298825e345cc09b826f5828bd8146e3600ca6e9981"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:6653c262ad71e6c0ae08eeca3af2ee89ad47483b6312f2c6094518cb77872406"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6c04f363cb4b316719421724521432b6e7f6490e5baaaf7692af961c28d0279b"}, - {file = "cramjam-2.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e30f1f00de913b440baa36647817b9b7120a69b04eca05f3354aaf5b40f95ee5"}, - {file = "cramjam-2.8.3.tar.gz", hash = "sha256:6b1fa0a6ea8183831d04572597c182bd6cece62d583a36cde1e6a86e72ce2389"}, -] - -[package.extras] -dev = ["black (==22.3.0)", "hypothesis", "numpy", "pytest (>=5.30)", "pytest-xdist"] - [[package]] name = "cycler" version = "0.12.1" @@ -907,13 +795,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2024.2.1" +version = "2023.12.0" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2024.2.1-py3-none-any.whl", hash = "sha256:a13fcdeead3bab3576495023f83097adcffe2f03c371c241b5a1f0b232b35b38"}, - {file = "dask-2024.2.1.tar.gz", hash = "sha256:9504a1e9f5d8e5403fae931f9f1660d41f510f48895ccefce856ec6a4c2198d8"}, + {file = "dask-2023.12.0-py3-none-any.whl", hash = "sha256:6cef2fa43815adc8fe895dd3ea473a311249ba0e15e1f7eb9b1b68302339d998"}, + {file = "dask-2023.12.0.tar.gz", hash = "sha256:3f687e647ced0d3f2cadbef113c730b6555b9499b62e3a220535438841001c91"}, ] [package.dependencies] @@ -931,7 +819,7 @@ array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] dataframe = ["dask[array]", "pandas (>=1.3)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.2.1)"] +distributed = ["distributed (==2023.12.0)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] @@ -1105,57 +993,6 @@ files = [ [package.extras] devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] -[[package]] -name = "fastparquet" -version = "2024.5.0" -description = "Python support for Parquet file format" -optional = false -python-versions = ">=3.9" -files = [ - {file = "fastparquet-2024.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9dfbed87b4b58b0794b2cb3aa4abcb43fc01480a10c7779a323d2dd1599f6acd"}, - {file = "fastparquet-2024.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07fc5a45450a39cd07c6ef0e0219ac4b1879f8b27c825ee4ba5d87a3ae505f11"}, - {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2045c21f90358541286f26f0735bfb2265b075413fbced3b876fc8848eda52"}, - {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f411056152b5d3cc82b6624d9da80535d10d9277d921fdb2e9516e93c8c227e8"}, - {file = "fastparquet-2024.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc99d7c0f1816394d53aadd47919bba70bb81355259d8788d28e35913816aee0"}, - {file = "fastparquet-2024.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:42149929b71d9122bd501aa695681f40a04a9fa3f5b802cf0fb6aa4e95ccf2dd"}, - {file = "fastparquet-2024.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5b1ed889f4ac7ea059ff95f4a01f5c07c825c50c2e1bc9e2b64c814df94c243"}, - {file = "fastparquet-2024.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:f5c3cabcfa2f534e4b23343c1ab84c37d336da73770005e608d1894ab1084600"}, - {file = "fastparquet-2024.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56d03b0a291d6a575ab365516c53b4da8e040347f8d43af79be25893c591b38c"}, - {file = "fastparquet-2024.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:784989ee2c251960b8f00dc38c6c730f784712c8e3d08cc7e0ce842055476af1"}, - {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d20bba5c39139a88d8d6931764b830ba14042742d802238d9edf86d4d765ad7a"}, - {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08358d99278c5d3fb523d819fff5c74d572d8f67ebbe2215a2c7bfca7e3664cf"}, - {file = "fastparquet-2024.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9de270e17a6ae2f02c716421d60e18d35d4718037f561b3e359989db19f700a"}, - {file = "fastparquet-2024.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba251231b005c0f3f7e56f6e9cd1939be99b2d810ab5b05039271e260c0196c6"}, - {file = "fastparquet-2024.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1496d83d7a77c19abae796e3b582539884fc893d75a3ad4f90df12f8f23a902a"}, - {file = "fastparquet-2024.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea3796c4a38ef8b372a3056b5cef52ca8182fa554fa51c7637c2421e69ee56e5"}, - {file = "fastparquet-2024.5.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1fa068ef1826bff6d4a9106a6f9e9d6fd20b8b516da4b82d87840cb5fd3947c"}, - {file = "fastparquet-2024.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a60f7b0b308d6b9f12c642cf5237a05d754926fb31ce865ff7072bceab19fbb"}, - {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6ac308a2f391ce589c99b8376e7cdfe4241ef5770ac4cf4c1c93f940bda83c"}, - {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b3cf7b4eb1b06e87b97a3a5c9124e4b1c08a8903ba017052c5fe2c482414a3d"}, - {file = "fastparquet-2024.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5626fc72204001b7e82fedb4b02174ecb4e2d4143b38b4ea8d2f9eb65f6b000e"}, - {file = "fastparquet-2024.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c8b2e86fe6488cce0e3d41263bb0296ef9bbb875a2fca09d67d7685640017a66"}, - {file = "fastparquet-2024.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2a951106782d51e5ab110beaad29c4aa0537f045711bb0bf146f65aeaed14174"}, - {file = "fastparquet-2024.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:47695037fdc534ef4247f25ccf17dcbd8825be6ecb70c54ca54d588a794f4a6d"}, - {file = "fastparquet-2024.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc3d35ff8341cd65baecac71062e9d73393d7afda207b3421709c1d3f4baa194"}, - {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:691348cc85890663dd3c0bb02544d38d4c07a0c3d68837324dc01007301150b5"}, - {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfdc8aaec67edd30814c2c2f0e291eb3c3044525d18c87e835ef8793d6e2ea2d"}, - {file = "fastparquet-2024.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0034d1b5af3a71cc2fb29c590f442c0b514f710d6d6996794ae375dcfe050c05"}, - {file = "fastparquet-2024.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b562be0f43a007493014512602ab6b0207d13ea4ae85e0d94d61febf08efa1ee"}, - {file = "fastparquet-2024.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:611da9043f9dab1c63e6c90a6b124e3d2789c34fefa00d45356517f1e8a09c83"}, - {file = "fastparquet-2024.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb93e8951f46943c8567c9a555cb3d24d2c78efdf78e95fd72177d80da73a10f"}, - {file = "fastparquet-2024.5.0.tar.gz", hash = "sha256:dffd1d0ac6e89e31c5b6dacf67a8d299d4afbbcf0bf8b797373904c819c48f51"}, -] - -[package.dependencies] -cramjam = ">=2.3" -fsspec = "*" -numpy = "*" -packaging = "*" -pandas = ">=1.5.0" - -[package.extras] -lzo = ["python-lzo"] - [[package]] name = "filelock" version = "3.15.3" @@ -2601,38 +2438,38 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -3008,10 +2845,10 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] @@ -3122,9 +2959,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -5119,9 +4956,10 @@ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linke test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] +all = [] notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "bb98b14e71e5f15860f0e61aa08637beb3e1a08973148f7ae000c2875f87093e" +content-hash = "5375409c845ef46117599d89d10fec86560fa1ceaaab7d2f0e9bbc0f4032db1c" diff --git a/pyproject.toml b/pyproject.toml index e315ee45..6d98d148 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9, <3.13" bokeh = ">=2.4.2" -dask = {version = ">=2021.12.0, <2024.3.0"} +dask = {version = ">=2021.12.0, <2023.12.1"} fastdtw = ">=0.3.4" h5py = ">=3.6.0" ipympl = ">=0.9.1" From 4065a3713e9e040287c18ddaa871a9fd9832a774 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 19:26:22 +0200 Subject: [PATCH 079/300] revert tests --- tests/calibrator/test_energy.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index bd9c3705..0c1735fb 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -616,7 +616,6 @@ def test_add_offsets_functionality(energy_scale: str) -> None: loader=get_loader("flash", config=config), ) res, meta = ec.add_offsets(t_df) - res = res.compute() exp_vals = df["energy"].copy() + 1 * scale_sign exp_vals += (df["off1"] - df["off1"].mean()) * scale_sign exp_vals -= df["off2"] * scale_sign @@ -633,7 +632,6 @@ def test_add_offsets_functionality(energy_scale: str) -> None: ) t_df = dask.dataframe.from_pandas(df.copy(), npartitions=2) res, meta = ec.add_offsets(t_df, **params) # type: ignore - res = res.compute() np.testing.assert_allclose(res["energy"].values, exp_vals.values) exp_meta = {} exp_meta["applied"] = True @@ -647,7 +645,6 @@ def test_add_offsets_functionality(energy_scale: str) -> None: t_df = dask.dataframe.from_pandas(df.copy(), npartitions=2) res, meta = ec.add_offsets(t_df, weights=-1, columns="off1") res, meta = ec.add_offsets(res, columns="off1") - res = res.compute() exp_vals = df["energy"].copy() np.testing.assert_allclose(res["energy"].values, exp_vals.values) exp_meta = {} From 24a353fc74596b267979bf91abf785331b0b0814 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 20:46:06 +0200 Subject: [PATCH 080/300] update annotations --- sed/core/processor.py | 251 ++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 129 deletions(-) diff --git a/sed/core/processor.py b/sed/core/processor.py index ee085739..f1632f74 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1,15 +1,13 @@ """This module contains the core class for the sed package """ +from __future__ import annotations + import pathlib +from collections.abc import Sequence from datetime import datetime from typing import Any from typing import cast -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as ddf import matplotlib.pyplot as plt @@ -48,11 +46,11 @@ class SedProcessor: Args: metadata (dict, optional): Dict of external Metadata. Defaults to None. - config (Union[dict, str], optional): Config dictionary or config file name. + config (dict | str, optional): Config dictionary or config file name. Defaults to None. - dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load + dataframe (pd.DataFrame | ddf.DataFrame, optional): dataframe to load into the class. Defaults to None. - files (List[str], optional): List of files to pass to the loader defined in + files (list[str], optional): List of files to pass to the loader defined in the config. Defaults to None. folder (str, optional): Folder containing files to pass to the loader defined in the config. Defaults to None. @@ -68,9 +66,9 @@ class SedProcessor: def __init__( self, metadata: dict = None, - config: Union[dict, str] = None, - dataframe: Union[pd.DataFrame, ddf.DataFrame] = None, - files: List[str] = None, + config: dict | str = None, + dataframe: pd.DataFrame | ddf.DataFrame = None, + files: list[str] = None, folder: str = None, runs: Sequence[str] = None, collect_metadata: bool = False, @@ -82,11 +80,11 @@ def __init__( Args: metadata (dict, optional): Dict of external Metadata. Defaults to None. - config (Union[dict, str], optional): Config dictionary or config file name. + config (dict | str, optional): Config dictionary or config file name. Defaults to None. - dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load + dataframe (pd.DataFrame | ddf.DataFrame, optional): dataframe to load into the class. Defaults to None. - files (List[str], optional): List of files to pass to the loader defined in + files (list[str], optional): List of files to pass to the loader defined in the config. Defaults to None. folder (str, optional): Folder containing files to pass to the loader defined in the config. Defaults to None. @@ -114,9 +112,9 @@ def __init__( else: self.verbose = verbose - self._dataframe: Union[pd.DataFrame, ddf.DataFrame] = None - self._timed_dataframe: Union[pd.DataFrame, ddf.DataFrame] = None - self._files: List[str] = [] + self._dataframe: pd.DataFrame | ddf.DataFrame = None + self._timed_dataframe: pd.DataFrame | ddf.DataFrame = None + self._files: list[str] = [] self._binned: xr.DataArray = None self._pre_binned: xr.DataArray = None @@ -208,20 +206,20 @@ def _repr_html_(self): # self.view_event_histogram(dfpid=2, backend="matplotlib") @property - def dataframe(self) -> Union[pd.DataFrame, ddf.DataFrame]: + def dataframe(self) -> pd.DataFrame | ddf.DataFrame: """Accessor to the underlying dataframe. Returns: - Union[pd.DataFrame, ddf.DataFrame]: Dataframe object. + pd.DataFrame | ddf.DataFrame: Dataframe object. """ return self._dataframe @dataframe.setter - def dataframe(self, dataframe: Union[pd.DataFrame, ddf.DataFrame]): + def dataframe(self, dataframe: pd.DataFrame | ddf.DataFrame): """Setter for the underlying dataframe. Args: - dataframe (Union[pd.DataFrame, ddf.DataFrame]): The dataframe object to set. + dataframe (pd.DataFrame | ddf.DataFrame): The dataframe object to set. """ if not isinstance(dataframe, (pd.DataFrame, ddf.DataFrame)) or not isinstance( dataframe, @@ -235,20 +233,20 @@ def dataframe(self, dataframe: Union[pd.DataFrame, ddf.DataFrame]): self._dataframe = dataframe @property - def timed_dataframe(self) -> Union[pd.DataFrame, ddf.DataFrame]: + def timed_dataframe(self) -> pd.DataFrame | ddf.DataFrame: """Accessor to the underlying timed_dataframe. Returns: - Union[pd.DataFrame, ddf.DataFrame]: Timed Dataframe object. + pd.DataFrame | ddf.DataFrame: Timed Dataframe object. """ return self._timed_dataframe @timed_dataframe.setter - def timed_dataframe(self, timed_dataframe: Union[pd.DataFrame, ddf.DataFrame]): + def timed_dataframe(self, timed_dataframe: pd.DataFrame | ddf.DataFrame): """Setter for the underlying timed dataframe. Args: - timed_dataframe (Union[pd.DataFrame, ddf.DataFrame]): The timed dataframe object to set + timed_dataframe (pd.DataFrame | ddf.DataFrame): The timed dataframe object to set """ if not isinstance(timed_dataframe, (pd.DataFrame, ddf.DataFrame)) or not isinstance( timed_dataframe, @@ -285,20 +283,20 @@ def add_attribute(self, attributes: dict, name: str, **kwds): ) @property - def config(self) -> Dict[Any, Any]: + def config(self) -> dict[Any, Any]: """Getter attribute for the config dictionary Returns: - Dict: The config dictionary. + dict: The config dictionary. """ return self._config @property - def files(self) -> List[str]: + def files(self) -> list[str]: """Getter attribute for the list of files Returns: - List[str]: The list of loaded files + list[str]: The list of loaded files """ return self._files @@ -337,17 +335,17 @@ def normalization_histogram(self) -> xr.DataArray: raise ValueError("No normalization histogram available, generate histogram first!") return self._normalization_histogram - def cpy(self, path: Union[str, List[str]]) -> Union[str, List[str]]: + def cpy(self, path: str | list[str]) -> str | list[str]: """Function to mirror a list of files or a folder from a network drive to a local storage. Returns either the original or the copied path to the given path. The option to use this functionality is set by config["core"]["use_copy_tool"]. Args: - path (Union[str, List[str]]): Source path or path list. + path (str | list[str]): Source path or path list. Returns: - Union[str, List[str]]: Source or destination path or path list. + str | list[str]: Source or destination path or path list. """ if self.use_copy_tool: if isinstance(path, list): @@ -365,9 +363,9 @@ def cpy(self, path: Union[str, List[str]]) -> Union[str, List[str]]: def load( self, - dataframe: Union[pd.DataFrame, ddf.DataFrame] = None, + dataframe: pd.DataFrame | ddf.DataFrame = None, metadata: dict = None, - files: List[str] = None, + files: list[str] = None, folder: str = None, runs: Sequence[str] = None, collect_metadata: bool = False, @@ -376,11 +374,11 @@ def load( """Load tabular data of single events into the dataframe object in the class. Args: - dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): data in tabular + dataframe (pd.DataFrame | ddf.DataFrame, optional): data in tabular format. Accepts anything which can be interpreted by pd.DataFrame as an input. Defaults to None. metadata (dict, optional): Dict of external Metadata. Defaults to None. - files (List[str], optional): List of file paths to pass to the loader. + files (list[str], optional): List of file paths to pass to the loader. Defaults to None. runs (Sequence[str], optional): List of run identifiers to pass to the loader. Defaults to None. @@ -425,7 +423,7 @@ def load( ) elif files is not None: dataframe, timed_dataframe, metadata = self.loader.read_dataframe( - files=cast(List[str], self.cpy(files)), + files=cast(list[str], self.cpy(files)), metadata=metadata, collect_metadata=collect_metadata, **kwds, @@ -490,10 +488,10 @@ def filter_column( # 1. Bin raw detector data for distortion correction def bin_and_load_momentum_calibration( self, - df_partitions: Union[int, Sequence[int]] = 100, - axes: List[str] = None, - bins: List[int] = None, - ranges: Sequence[Tuple[float, float]] = None, + df_partitions: int | Sequence[int] = 100, + axes: list[str] = None, + bins: list[int] = None, + ranges: Sequence[tuple[float, float]] = None, plane: int = 0, width: int = 5, apply: bool = False, @@ -504,13 +502,13 @@ def bin_and_load_momentum_calibration( interactive view, and load it into the momentum corrector class. Args: - df_partitions (Union[int, Sequence[int]], optional): Number of dataframe partitions + df_partitions (int | Sequence[int], optional): Number of dataframe partitions to use for the initial binning. Defaults to 100. - axes (List[str], optional): Axes to bin. + axes (list[str], optional): Axes to bin. Defaults to config["momentum"]["axes"]. - bins (List[int], optional): Bin numbers to use for binning. + bins (list[int], optional): Bin numbers to use for binning. Defaults to config["momentum"]["bins"]. - ranges (List[Tuple], optional): Ranges to use for binning. + ranges (Sequence[tuple[float, float]], optional): Ranges to use for binning. Defaults to config["momentum"]["ranges"]. plane (int, optional): Initial value for the plane slider. Defaults to 0. width (int, optional): Initial value for the width slider. Defaults to 5. @@ -674,7 +672,7 @@ def save_splinewarp( # scaling, shift and rotation def pose_adjustment( self, - transformations: Dict[str, Any] = None, + transformations: dict[str, Any] = None, apply: bool = False, use_correction: bool = True, reset: bool = True, @@ -687,7 +685,7 @@ def pose_adjustment( the image. Args: - transformations (dict, optional): Dictionary with transformations. + transformations (dict[str, Any], optional): Dictionary with transformations. Defaults to self.transformations or config["momentum"]["transformtions"]. apply (bool, optional): Option to directly apply the provided transformations. Defaults to False. @@ -833,11 +831,11 @@ def apply_momentum_correction( # 1. Calculate momentum calibration def calibrate_momentum_axes( self, - point_a: Union[np.ndarray, List[int]] = None, - point_b: Union[np.ndarray, List[int]] = None, + point_a: np.ndarray | list[int] = None, + point_b: np.ndarray | list[int] = None, k_distance: float = None, - k_coord_a: Union[np.ndarray, List[float]] = None, - k_coord_b: Union[np.ndarray, List[float]] = np.array([0.0, 0.0]), + k_coord_a: np.ndarray | list[float] = None, + k_coord_b: np.ndarray | list[float] = np.array([0.0, 0.0]), equiscale: bool = True, apply=False, ): @@ -848,18 +846,18 @@ def calibrate_momentum_axes( the points. Args: - point_a (Union[np.ndarray, List[int]]): Pixel coordinates of the first + point_a (np.ndarray | list[int], optional): Pixel coordinates of the first point used for momentum calibration. - point_b (Union[np.ndarray, List[int]], optional): Pixel coordinates of the + point_b (np.ndarray | list[int], optional): Pixel coordinates of the second point used for momentum calibration. Defaults to config["momentum"]["center_pixel"]. k_distance (float, optional): Momentum distance between point a and b. Needs to be provided if no specific k-koordinates for the two points are given. Defaults to None. - k_coord_a (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_a (np.ndarray | list[float], optional): Momentum coordinate of the first point used for calibration. Used if equiscale is False. Defaults to None. - k_coord_b (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_b (np.ndarray | list[float], optional): Momentum coordinate of the second point used for calibration. Defaults to [0.0, 0.0]. equiscale (bool, optional): Option to apply different scales to kx and ky. If True, the distance between points a and b, and the absolute @@ -987,7 +985,7 @@ def adjust_energy_correction( self, correction_type: str = None, amplitude: float = None, - center: Tuple[float, float] = None, + center: tuple[float, float] = None, apply=False, **kwds, ): @@ -1007,7 +1005,7 @@ def adjust_energy_correction( Defaults to config["energy"]["correction_type"]. amplitude (float, optional): Amplitude of the correction. Defaults to config["energy"]["correction"]["amplitude"]. - center (Tuple[float, float], optional): Center X/Y coordinates for the + center (tuple[float, float], optional): Center X/Y coordinates for the correction. Defaults to config["energy"]["correction"]["center"]. apply (bool, optional): Option to directly apply the provided or default correction parameters. Defaults to False. @@ -1125,11 +1123,11 @@ def apply_energy_correction( # 1. Load and normalize data def load_bias_series( self, - binned_data: Union[xr.DataArray, Tuple[np.ndarray, np.ndarray, np.ndarray]] = None, - data_files: List[str] = None, - axes: List[str] = None, - bins: List = None, - ranges: Sequence[Tuple[float, float]] = None, + binned_data: xr.DataArray | tuple[np.ndarray, np.ndarray, np.ndarray] = None, + data_files: list[str] = None, + axes: list[str] = None, + bins: list = None, + ranges: Sequence[tuple[float, float]] = None, biases: np.ndarray = None, bias_key: str = None, normalize: bool = None, @@ -1140,16 +1138,16 @@ def load_bias_series( single-event files, or load binned bias/TOF traces. Args: - binned_data (Union[xr.DataArray, Tuple[np.ndarray, np.ndarray, np.ndarray]], optional): + binned_data (xr.DataArray | tuple[np.ndarray, np.ndarray, np.ndarray], optional): Binned data If provided as DataArray, Needs to contain dimensions config["dataframe"]["tof_column"] and config["dataframe"]["bias_column"]. If provided as tuple, needs to contain elements tof, biases, traces. - data_files (List[str], optional): list of file paths to bin - axes (List[str], optional): bin axes. + data_files (list[str], optional): list of file paths to bin + axes (list[str], optional): bin axes. Defaults to config["dataframe"]["tof_column"]. - bins (List, optional): number of bins. + bins (list, optional): number of bins. Defaults to config["energy"]["bins"]. - ranges (Sequence[Tuple[float, float]], optional): bin ranges. + ranges (Sequence[tuple[float, float]], optional): bin ranges. Defaults to config["energy"]["ranges"]. biases (np.ndarray, optional): Bias voltages used. If missing, bias voltages are extracted from the data files. @@ -1190,7 +1188,7 @@ def load_bias_series( elif data_files is not None: self.ec.bin_data( - data_files=cast(List[str], self.cpy(data_files)), + data_files=cast(list[str], self.cpy(data_files)), axes=axes, bins=bins, ranges=ranges, @@ -1218,7 +1216,7 @@ def load_bias_series( # 2. extract ranges and get peak positions def find_bias_peaks( self, - ranges: Union[List[Tuple], Tuple], + ranges: list[tuple] | tuple, ref_id: int = 0, infer_others: bool = True, mode: str = "replace", @@ -1234,7 +1232,7 @@ def find_bias_peaks( Alternatively, a list of ranges for all traces can be provided. Args: - ranges (Union[List[Tuple], Tuple]): Tuple of TOF values indicating a range. + ranges (list[tuple] | tuple): Tuple of TOF values indicating a range. Alternatively, a list of ranges for all traces can be given. ref_id (int, optional): The id of the trace the range refers to. Defaults to 0. @@ -1482,10 +1480,10 @@ def append_energy_axis( def add_energy_offset( self, constant: float = None, - columns: Union[str, Sequence[str]] = None, - weights: Union[float, Sequence[float]] = None, - reductions: Union[str, Sequence[str]] = None, - preserve_mean: Union[bool, Sequence[bool]] = None, + columns: str | Sequence[str] = None, + weights: float | Sequence[float] = None, + reductions: str | Sequence[str] = None, + preserve_mean: bool | Sequence[bool] = None, preview: bool = False, verbose: bool = None, ) -> None: @@ -1493,15 +1491,16 @@ def add_energy_offset( Args: constant (float, optional): The constant to shift the energy axis by. - columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from. - weights (Union[float, Sequence[float]]): weights to apply to the columns. + columns (str | Sequence[str]): Name of the column(s) to apply the shift from. + weights (float | Sequence[float]): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - preserve_mean (bool): Whether to subtract the mean of the column before applying the - shift. Defaults to False. - reductions (str): The reduction to apply to the column. Should be an available method - of dask.dataframe.Series. For example "mean". In this case the function is applied - to the column to generate a single value for the whole dataset. If None, the shift - is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. + reductions (str | Sequence[str]): The reduction to apply to the column. Should be an + available method of dask.dataframe.Series. For example "mean". In this case the + function is applied to the column to generate a single value for the whole dataset. + If None, the shift is applied per-dataframe-row. Defaults to None. Currently only + "mean" is supported. + preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column + before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. @@ -1699,7 +1698,7 @@ def align_dld_sectors( # Delay calibration function def calibrate_delay_axis( self, - delay_range: Tuple[float, float] = None, + delay_range: tuple[float, float] = None, datafile: str = None, preview: bool = False, verbose: bool = None, @@ -1709,7 +1708,7 @@ def calibrate_delay_axis( them from a file. Args: - delay_range (Tuple[float, float], optional): The scanned delay range in + delay_range (tuple[float, float], optional): The scanned delay range in picoseconds. Defaults to None. datafile (str, optional): The file from which to read the delay ranges. Defaults to None. @@ -1815,10 +1814,10 @@ def add_delay_offset( self, constant: float = None, flip_delay_axis: bool = None, - columns: Union[str, Sequence[str]] = None, - weights: Union[float, Sequence[float]] = 1.0, - reductions: Union[str, Sequence[str]] = None, - preserve_mean: Union[bool, Sequence[bool]] = False, + columns: str | Sequence[str] = None, + weights: float | Sequence[float] = 1.0, + reductions: str | Sequence[str] = None, + preserve_mean: bool | Sequence[bool] = False, preview: bool = False, verbose: bool = None, ) -> None: @@ -1827,15 +1826,16 @@ def add_delay_offset( Args: constant (float, optional): The constant to shift the delay axis by. flip_delay_axis (bool, optional): Option to reverse the direction of the delay axis. - columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from. - weights (Union[float, Sequence[float]]): weights to apply to the columns. + columns (str | Sequence[str]): Name of the column(s) to apply the shift from. + weights (float | Sequence[float]): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - preserve_mean (bool): Whether to subtract the mean of the column before applying the - shift. Defaults to False. - reductions (str): The reduction to apply to the column. Should be an available method - of dask.dataframe.Series. For example "mean". In this case the function is applied - to the column to generate a single value for the whole dataset. If None, the shift - is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. + reductions (str | Sequence[str]): The reduction to apply to the column. Should be an + available method of dask.dataframe.Series. For example "mean". In this case the + function is applied to the column to generate a single value for the whole dataset. + If None, the shift is applied per-dataframe-row. Defaults to None. Currently only + "mean" is supported. + preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column + before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. @@ -1953,16 +1953,16 @@ def save_workflow_params( def add_jitter( self, - cols: List[str] = None, - amps: Union[float, Sequence[float]] = None, + cols: list[str] = None, + amps: float | Sequence[float] = None, **kwds, ): """Add jitter to the selected dataframe columns. Args: - cols (List[str], optional): The colums onto which to apply jitter. + cols (list[str], optional): The colums onto which to apply jitter. Defaults to config["dataframe"]["jitter_cols"]. - amps (Union[float, Sequence[float]], optional): Amplitude scalings for the + amps (float | Sequence[float], optional): Amplitude scalings for the jittering noise. If one number is given, the same is used for all axes. For uniform noise (default) it will cover the interval [-amp, +amp]. Defaults to config["dataframe"]["jitter_amps"]. @@ -2065,7 +2065,7 @@ def add_time_stamped_data( time_stamp_column=time_stamp_column, **kwds, ) - metadata: List[Any] = [] + metadata: list[Any] = [] metadata.append(dest_column) metadata.append(time_stamps) metadata.append(data) @@ -2073,22 +2073,22 @@ def add_time_stamped_data( def pre_binning( self, - df_partitions: Union[int, Sequence[int]] = 100, - axes: List[str] = None, - bins: List[int] = None, - ranges: Sequence[Tuple[float, float]] = None, + df_partitions: int | Sequence[int] = 100, + axes: list[str] = None, + bins: list[int] = None, + ranges: Sequence[tuple[float, float]] = None, **kwds, ) -> xr.DataArray: """Function to do an initial binning of the dataframe loaded to the class. Args: - df_partitions (Union[int, Sequence[int]], optional): Number of dataframe partitions to + df_partitions (int | Sequence[int], optional): Number of dataframe partitions to use for the initial binning. Defaults to 100. - axes (List[str], optional): Axes to bin. + axes (list[str], optional): Axes to bin. Defaults to config["momentum"]["axes"]. - bins (List[int], optional): Bin numbers to use for binning. + bins (list[int], optional): Bin numbers to use for binning. Defaults to config["momentum"]["bins"]. - ranges (List[Tuple], optional): Ranges to use for binning. + ranges (Sequence[tuple[float, float]], optional): Ranges to use for binning. Defaults to config["momentum"]["ranges"]. **kwds: Keyword argument passed to ``compute``. @@ -2108,7 +2108,7 @@ def pre_binning( ranges_[2] = np.asarray(ranges_[2]) / 2 ** ( self._config["dataframe"]["tof_binning"] - 1 ) - ranges = [cast(Tuple[float, float], tuple(v)) for v in ranges_] + ranges = [cast(tuple[float, float], tuple(v)) for v in ranges_] assert self._dataframe is not None, "dataframe needs to be loaded first!" @@ -2122,23 +2122,16 @@ def pre_binning( def compute( self, - bins: Union[ - int, - dict, - tuple, - List[int], - List[np.ndarray], - List[tuple], - ] = 100, - axes: Union[str, Sequence[str]] = None, - ranges: Sequence[Tuple[float, float]] = None, - normalize_to_acquisition_time: Union[bool, str] = False, + bins: int | dict | tuple | list[int] | list[np.ndarray] | list[tuple] = 100, + axes: str | Sequence[str] = None, + ranges: Sequence[tuple[float, float]] = None, + normalize_to_acquisition_time: bool | str = False, **kwds, ) -> xr.DataArray: """Compute the histogram along the given dimensions. Args: - bins (int, dict, tuple, List[int], List[np.ndarray], List[tuple], optional): + bins (int | dict | tuple | list[int] | list[np.ndarray] | list[tuple], optional): Definition of the bins. Can be any of the following cases: - an integer describing the number of bins in on all dimensions @@ -2149,12 +2142,12 @@ def compute( - a dictionary made of the axes as keys and any of the above as values. This takes priority over the axes and range arguments. Defaults to 100. - axes (Union[str, Sequence[str]], optional): The names of the axes (columns) + axes (str | Sequence[str], optional): The names of the axes (columns) on which to calculate the histogram. The order will be the order of the dimensions in the resulting array. Defaults to None. - ranges (Sequence[Tuple[float, float]], optional): list of tuples containing + ranges (Sequence[tuple[float, float]], optional): list of tuples containing the start and end point of the binning range. Defaults to None. - normalize_to_acquisition_time (Union[bool, str]): Option to normalize the + normalize_to_acquisition_time (bool | str): Option to normalize the result to the acquistion time. If a "slow" axis was scanned, providing the name of the scanned axis will compute and apply the corresponding normalization histogram. Defaults to False. @@ -2205,7 +2198,7 @@ def compute( "threadpool_API", self._config["binning"]["threadpool_API"], ) - df_partitions: Union[int, Sequence[int]] = kwds.pop("df_partitions", None) + df_partitions: int | Sequence[int] = kwds.pop("df_partitions", None) if isinstance(df_partitions, int): df_partitions = list(range(0, min(df_partitions, self._dataframe.npartitions))) if df_partitions is not None: @@ -2327,7 +2320,7 @@ def get_normalization_histogram( if axis not in self._binned.coords: raise ValueError(f"Axis '{axis}' not found in binned data!") - df_partitions: Union[int, Sequence[int]] = kwds.pop("df_partitions", None) + df_partitions: int | Sequence[int] = kwds.pop("df_partitions", None) if isinstance(df_partitions, int): df_partitions = list(range(0, min(df_partitions, self._dataframe.npartitions))) if use_time_stamps or self._timed_dataframe is None: @@ -2369,7 +2362,7 @@ def view_event_histogram( ncol: int = 2, bins: Sequence[int] = None, axes: Sequence[str] = None, - ranges: Sequence[Tuple[float, float]] = None, + ranges: Sequence[tuple[float, float]] = None, backend: str = "bokeh", legend: bool = True, histkwds: dict = None, @@ -2386,7 +2379,7 @@ def view_event_histogram( axes. Defaults to config["histogram"]["bins"]. axes (Sequence[str], optional): Names of the axes to display. Defaults to config["histogram"]["axes"]. - ranges (Sequence[Tuple[float, float]], optional): Value ranges of all + ranges (Sequence[tuple[float, float]], optional): Value ranges of all specified axes. Defaults toconfig["histogram"]["ranges"]. backend (str, optional): Backend of the plotting library ('matplotlib' or 'bokeh'). Defaults to "bokeh". From 867a7113b250230314ac8150038ad3c837793a20 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 21:49:13 +0200 Subject: [PATCH 081/300] more typing fixes --- .pre-commit-config.yaml | 6 +-- sed/binning/binning.py | 69 ++++++++++++++-------------------- sed/binning/numba_bin.py | 24 ++++++------ sed/binning/utils.py | 30 ++++++--------- sed/calibrator/delay.py | 81 ++++++++++++++++++++-------------------- sed/diagnostics.py | 9 +++-- 6 files changed, 98 insertions(+), 121 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f535dab..208dcfd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,12 +27,12 @@ repos: rev: v3.8.2 hooks: - id: reorder-python-imports - args: [--application-directories, '.:src', --py36-plus] + args: [--application-directories, '.:src', --py39-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.16.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py39-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: diff --git a/sed/binning/binning.py b/sed/binning/binning.py index 0f5a1824..69a5c44b 100644 --- a/sed/binning/binning.py +++ b/sed/binning/binning.py @@ -1,12 +1,11 @@ """This module contains the binning functions of the sed.binning module - """ +from __future__ import annotations + import gc +from collections.abc import Sequence from functools import reduce from typing import cast -from typing import List -from typing import Sequence -from typing import Tuple from typing import Union import dask.dataframe @@ -26,27 +25,21 @@ def bin_partition( - part: Union[dask.dataframe.DataFrame, pd.DataFrame], - bins: Union[ - int, - dict, - Sequence[int], - Sequence[np.ndarray], - Sequence[tuple], - ] = 100, + part: dask.dataframe.DataFrame | pd.DataFrame, + bins: int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple] = 100, axes: Sequence[str] = None, - ranges: Sequence[Tuple[float, float]] = None, + ranges: Sequence[tuple[float, float]] = None, hist_mode: str = "numba", - jitter: Union[list, dict] = None, + jitter: list | dict = None, return_edges: bool = False, skip_test: bool = False, -) -> Union[np.ndarray, Tuple[np.ndarray, list]]: +) -> np.ndarray | tuple[np.ndarray, list]: """Compute the n-dimensional histogram of a single dataframe partition. Args: - part (Union[dask.dataframe.DataFrame, pd.DataFrame]): dataframe on which + part (dask.dataframe.DataFrame | pd.DataFrame): dataframe on which to perform the histogram. Usually a partition of a dask DataFrame. - bins (int, dict, Sequence[int], Sequence[np.ndarray], Sequence[tuple], optional): + bins (int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple], optional): Definition of the bins. Can be any of the following cases: - an integer describing the number of bins for all dimensions. This @@ -70,7 +63,7 @@ def bin_partition( the order of the dimensions in the resulting array. Only not required if bins are provided as dictionary containing the axis names. Defaults to None. - ranges (Sequence[Tuple[float, float]], optional): Sequence of tuples containing + ranges (Sequence[tuple[float, float]], optional): Sequence of tuples containing the start and end point of the binning range. Required if bins given as int or Sequence[int]. Defaults to None. hist_mode (str, optional): Histogram calculation method. @@ -79,7 +72,7 @@ def bin_partition( - "numba" use a numba powered similar method. Defaults to "numba". - jitter (Union[list, dict], optional): a list of the axes on which to apply + jitter (list | dict, optional): a list of the axes on which to apply jittering. To specify the jitter amplitude or method (normal or uniform noise) a dictionary can be passed. This should look like jitter={'axis':{'amplitude':0.5,'mode':'uniform'}}. @@ -102,8 +95,8 @@ def bin_partition( present in the dataframe Returns: - Union[np.ndarray, Tuple[np.ndarray, list]]: 2-element tuple returned only when - returnEdges is True. Otherwise only hist is returned. + np.ndarray | tuple[np.ndarray: 2-element tuple returned only when + return_edges is True. Otherwise only hist is returned. - **hist**: The result of the n-dimensional binning - **edges**: A list of D arrays describing the bin edges for each dimension. @@ -122,17 +115,17 @@ def bin_partition( raise TypeError( "axes needs to be of type 'List[str]' if tests are skipped!", ) - bins = cast(Union[List[int], List[np.ndarray]], bins) - axes = cast(List[str], axes) - ranges = cast(List[Tuple[float, float]], ranges) + bins = cast(Union[list[int], list[np.ndarray]], bins) + axes = cast(list[str], axes) + ranges = cast(list[tuple[float, float]], ranges) # convert bin centers to bin edges: if all(isinstance(x, np.ndarray) for x in bins): - bins = cast(List[np.ndarray], bins) + bins = cast(list[np.ndarray], bins) for i, bin_centers in enumerate(bins): bins[i] = bin_centers_to_bin_edges(bin_centers) else: - bins = cast(List[int], bins) + bins = cast(list[int], bins) # shift ranges by half a bin size to align the bin centers to the given ranges, # as the histogram functions interprete the ranges as limits for the edges. for i, nbins in enumerate(bins): @@ -203,18 +196,12 @@ def bin_partition( def bin_dataframe( df: dask.dataframe.DataFrame, - bins: Union[ - int, - dict, - Sequence[int], - Sequence[np.ndarray], - Sequence[tuple], - ] = 100, + bins: int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple] = 100, axes: Sequence[str] = None, - ranges: Sequence[Tuple[float, float]] = None, + ranges: Sequence[tuple[float, float]] = None, hist_mode: str = "numba", mode: str = "fast", - jitter: Union[list, dict] = None, + jitter: list | dict = None, pbar: bool = True, n_cores: int = N_CPU - 1, threads_per_worker: int = 4, @@ -228,7 +215,7 @@ def bin_dataframe( Args: df (dask.dataframe.DataFrame): a dask.DataFrame on which to perform the histogram. - bins (int, dict, Sequence[int], Sequence[np.ndarray], Sequence[tuple], optional): + bins (int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple], optional): Definition of the bins. Can be any of the following cases: - an integer describing the number of bins for all dimensions. This @@ -252,7 +239,7 @@ def bin_dataframe( the order of the dimensions in the resulting array. Only not required if bins are provided as dictionary containing the axis names. Defaults to None. - ranges (Sequence[Tuple[float, float]], optional): Sequence of tuples containing + ranges (Sequence[tuple[float, float]], optional): Sequence of tuples containing the start and end point of the binning range. Required if bins given as int or Sequence[int]. Defaults to None. hist_mode (str, optional): Histogram calculation method. @@ -269,7 +256,7 @@ def bin_dataframe( - 'legacy': Single-core recombination of partition results. Defaults to "fast". - jitter (Union[list, dict], optional): a list of the axes on which to apply + jitter (list | dict, optional): a list of the axes on which to apply jittering. To specify the jitter amplitude or method (normal or uniform noise) a dictionary can be passed. This should look like jitter={'axis':{'amplitude':0.5,'mode':'uniform'}}. @@ -304,14 +291,14 @@ def bin_dataframe( # create the coordinate axes for the xarray output # if provided as array, they are interpreted as bin centers if isinstance(bins[0], np.ndarray): - bins = cast(List[np.ndarray], bins) + bins = cast(list[np.ndarray], bins) coords = dict(zip(axes, bins)) elif ranges is None: raise ValueError( "bins is not an array and range is none. this shouldn't happen.", ) else: - bins = cast(List[int], bins) + bins = cast(list[int], bins) coords = { ax: np.linspace(r[0], r[1], n, endpoint=False) for ax, r, n in zip(axes, ranges, bins) } @@ -509,7 +496,7 @@ def normalization_histogram_from_timed_dataframe( def apply_jitter_on_column( - df: Union[dask.dataframe.core.DataFrame, pd.DataFrame], + df: dask.dataframe.core.DataFrame | pd.DataFrame, amp: float, col: str, mode: str = "uniform", diff --git a/sed/binning/numba_bin.py b/sed/binning/numba_bin.py index 28530c12..24f79c57 100644 --- a/sed/binning/numba_bin.py +++ b/sed/binning/numba_bin.py @@ -1,13 +1,11 @@ """This file contains code for binning using numba precompiled code for the sed.binning module - """ +from __future__ import annotations + +from collections.abc import Sequence from typing import Any from typing import cast -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import numba import numpy as np @@ -108,7 +106,7 @@ def binsearch(bins: np.ndarray, val: float) -> int: def _hist_from_bins( sample: np.ndarray, bins: Sequence[np.ndarray], - shape: Tuple, + shape: tuple, ) -> np.ndarray: """Numba powered binning method, similar to np.histogramdd. @@ -118,7 +116,7 @@ def _hist_from_bins( sample (np.ndarray) : the array of shape (N,D) on which to compute the histogram bins (Sequence[np.ndarray]): array of shape (N,D) defining the D bins on which to compute the histogram, i.e. the desired output axes. - shape (Tuple): shape of the resulting array. Workaround for the fact numba + shape (tuple): shape of the resulting array. Workaround for the fact numba does not allow to create tuples. Returns: hist: the computed n-dimensional histogram @@ -154,9 +152,9 @@ def _hist_from_bins( def numba_histogramdd( sample: np.ndarray, - bins: Union[int, Sequence[int], Sequence[np.ndarray], np.ndarray], + bins: int | Sequence[int] | Sequence[np.ndarray] | np.ndarray, ranges: Sequence = None, -) -> Tuple[np.ndarray, List[np.ndarray]]: +) -> tuple[np.ndarray, list[np.ndarray]]: """Multidimensional histogramming function, powered by Numba. Behaves in total much like numpy.histogramdd. Returns uint32 arrays. @@ -168,7 +166,7 @@ def numba_histogramdd( Args: sample (np.ndarray): The data to be histogrammed with shape N,D - bins (Union[int, Sequence[int], Sequence[np.ndarray], np.ndarray]): The number + bins (int | Sequence[int] | Sequence[np.ndarray] | np.ndarray): The number of bins for each dimension D, or a sequence of bin edges on which to calculate the histogram. ranges (Sequence, optional): The range(s) to use for binning when bins is a sequence @@ -181,7 +179,7 @@ def numba_histogramdd( RuntimeError: Internal shape error after binning Returns: - Tuple[np.ndarray, List[np.ndarray]]: 2-element tuple of The computed histogram + tuple[np.ndarray, list[np.ndarray]]: 2-element tuple of The computed histogram and s list of D arrays describing the bin edges for each dimension. - **hist**: The computed histogram @@ -213,7 +211,7 @@ def numba_histogramdd( # method == "array" if isinstance(bins[0], np.ndarray): - bins = cast(List[np.ndarray], list(bins)) + bins = cast(list[np.ndarray], list(bins)) hist = _hist_from_bins( sample, tuple(bins), @@ -239,7 +237,7 @@ def numba_histogramdd( bins = tuple(bins) # Create edge arrays - edges: List[Any] = [] + edges: list[Any] = [] nbin = np.empty(num_cols, int) for i in range(num_cols): diff --git a/sed/binning/utils.py b/sed/binning/utils.py index d791c8b8..6d0acc7a 100644 --- a/sed/binning/utils.py +++ b/sed/binning/utils.py @@ -1,11 +1,9 @@ """This file contains helper functions for the sed.binning module - """ +from __future__ import annotations + +from collections.abc import Sequence from typing import cast -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import numpy as np @@ -16,16 +14,10 @@ def _arraysum(array_a, array_b): def simplify_binning_arguments( - bins: Union[ - int, - dict, - Sequence[int], - Sequence[np.ndarray], - Sequence[tuple], - ], + bins: int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple], axes: Sequence[str] = None, - ranges: Sequence[Tuple[float, float]] = None, -) -> Tuple[Union[List[int], List[np.ndarray]], List[str], List[Tuple[float, float]]]: + ranges: Sequence[tuple[float, float]] = None, +) -> tuple[list[int] | list[np.ndarray], list[str], list[tuple[float, float]]]: """Convert the flexible input for defining bins into a simple "axes" "bins" "ranges" tuple. @@ -33,7 +25,7 @@ def simplify_binning_arguments( binning functions defined here. Args: - bins (int, dict, Sequence[int], Sequence[np.ndarray], Sequence[tuple]): + bins (int | dict | Sequence[int] | Sequence[np.ndarray] | Sequence[tuple]): Definition of the bins. Can be any of the following cases: - an integer describing the number of bins for all dimensions. This @@ -56,7 +48,7 @@ def simplify_binning_arguments( the order of the dimensions in the resulting array. Only not required if bins are provided as dictionary containing the axis names. Defaults to None. - ranges (Sequence[Tuple[float, float]], optional): Sequence of tuples containing + ranges (Sequence[tuple[float, float]], optional): Sequence of tuples containing the start and end point of the binning range. Required if bins given as int or Sequence[int]. Defaults to None. @@ -67,7 +59,7 @@ def simplify_binning_arguments( AttributeError: Shape mismatch Returns: - Tuple[Union[List[int], List[np.ndarray]], List[Tuple[float, float]]]: Tuple + tuple[list[int] | list[np.ndarray], list[str], list[tuple[float, float]]]: Tuple containing lists of bin centers, axes, and ranges. """ # if bins is a dictionary: unravel to axes and bins @@ -113,7 +105,7 @@ def simplify_binning_arguments( # if bins are provided as int, check that ranges are present if all(isinstance(x, (int, np.int64)) for x in bins): - bins = cast(List[int], list(bins)) + bins = cast(list[int], list(bins)) if ranges is None: raise AttributeError( "Must provide a range if bins is an integer or list of integers", @@ -125,7 +117,7 @@ def simplify_binning_arguments( # otherwise, all bins should by np.ndarrays here elif all(isinstance(x, np.ndarray) for x in bins): - bins = cast(List[np.ndarray], list(bins)) + bins = cast(list[np.ndarray], list(bins)) else: raise TypeError(f"Could not interpret bins of type {type(bins)}") diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 55fabd06..e6d40af6 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -1,13 +1,11 @@ """sed.calibrator.delay module. Code for delay calibration. """ +from __future__ import annotations + +from collections.abc import Sequence from copy import deepcopy from datetime import datetime from typing import Any -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe import h5py @@ -46,30 +44,30 @@ def __init__( "corrected_delay_column", self.delay_column, ) - self.calibration: Dict[str, Any] = self._config["delay"].get("calibration", {}) - self.offsets: Dict[str, Any] = self._config["delay"].get("offsets", {}) + self.calibration: dict[str, Any] = self._config["delay"].get("calibration", {}) + self.offsets: dict[str, Any] = self._config["delay"].get("offsets", {}) def append_delay_axis( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, adc_column: str = None, delay_column: str = None, - calibration: Dict[str, Any] = None, - adc_range: Union[Tuple, List, np.ndarray] = None, - delay_range: Union[Tuple, List, np.ndarray] = None, + calibration: dict[str, Any] = None, + adc_range: tuple | list | np.ndarray = None, + delay_range: tuple | list | np.ndarray = None, time0: float = None, - delay_range_mm: Union[Tuple, List, np.ndarray] = None, + delay_range_mm: tuple | list | np.ndarray = None, datafile: str = None, p1_key: str = None, p2_key: str = None, t0_key: str = None, verbose: bool = True, - ) -> Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the delay axis to the events dataframe, by converting values from an analog-digital-converter (ADC). Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): The dataframe where + df (pd.DataFrame | dask.dataframe.DataFrame): The dataframe where to apply the delay calibration to. adc_column (str, optional): Source column for delay calibration. Defaults to config["dataframe"]["adc_column"]. @@ -77,14 +75,14 @@ def append_delay_axis( Defaults to config["dataframe"]["delay_column"]. calibration (dict, optional): Calibration dictionary with parameters for delay calibration. - adc_range (Union[Tuple, List, np.ndarray], optional): The range of used + adc_range (tuple | list | np.ndarray, optional): The range of used ADC values. Defaults to config["delay"]["adc_range"]. - delay_range (Union[Tuple, List, np.ndarray], optional): Range of scanned + delay_range (tuple | list | np.ndarray, optional): Range of scanned delay values in ps. If omitted, the range is calculated from the delay_range_mm and t0 values. time0 (float, optional): Pump-Probe overlap value of the delay coordinate. If omitted, it is searched for in the data files. - delay_range_mm (Union[Tuple, List, np.ndarray], optional): Range of scanned + delay_range_mm (tuple | list | np.ndarray, optional): Range of scanned delay stage in mm. If omitted, it is searched for in the data files. datafile (str, optional): Datafile in which delay parameters are searched for. Defaults to None. @@ -102,7 +100,7 @@ def append_delay_axis( NotImplementedError: Raised if no sufficient information passed. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: dataframe with added column + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: dataframe with added column and delay calibration metdata dictionary. """ # pylint: disable=duplicate-code @@ -207,39 +205,40 @@ def append_delay_axis( def add_offsets( self, df: dask.dataframe.DataFrame, - offsets: Dict[str, Any] = None, + offsets: dict[str, Any] = None, constant: float = None, flip_delay_axis: bool = None, - columns: Union[str, Sequence[str]] = None, - weights: Union[float, Sequence[float]] = 1.0, - preserve_mean: Union[bool, Sequence[bool]] = False, - reductions: Union[str, Sequence[str]] = None, + columns: str | Sequence[str] = None, + weights: float | Sequence[float] = 1.0, + preserve_mean: bool | Sequence[bool] = False, + reductions: str | Sequence[str] = None, delay_column: str = None, verbose: bool = True, - ) -> Tuple[dask.dataframe.DataFrame, dict]: + ) -> tuple[dask.dataframe.DataFrame, dict]: """Apply an offset to the delay column based on a constant or other columns. Args: df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. - offsets (Dict, optional): Dictionary of delay offset parameters. + offsets (dict, optional): Dictionary of delay offset parameters. constant (float, optional): The constant to shift the delay axis by. flip_delay_axis (bool, optional): Whether to flip the time axis. Defaults to False. - columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from. - weights (Union[int, Sequence[int]]): weights to apply to the columns. + columns (str | Sequence[str]): Name of the column(s) to apply the shift from. + weights (float | Sequence[float]): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - preserve_mean (bool): Whether to subtract the mean of the column before applying the - shift. Defaults to False. - reductions (str): The reduction to apply to the column. Should be an available method - of dask.dataframe.Series. For example "mean". In this case the function is applied - to the column to generate a single value for the whole dataset. If None, the shift - is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. + preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column + before applying the shift. Defaults to False. + reductions (str | Sequence[str]): The reduction to apply to the column. Should be an + available method of dask.dataframe.Series. For example "mean". In this case the + function is applied to the column to generate a single value for the whole dataset. + If None, the shift is applied per-dataframe-row. Defaults to None. Currently only + "mean" is supported. delay_column (str, optional): Name of the column containing the delay values. verbose (bool, optional): Option to print out diagnostic information. Defaults to True. Returns: - dask.dataframe.DataFrame: Dataframe with the shifted delay axis. - dict: Metadata dictionary. + tuple[dask.dataframe.DataFrame, dict]: Dataframe with the shifted delay axis and + Metadata dictionary. """ if offsets is None: offsets = deepcopy(self.offsets) @@ -247,7 +246,7 @@ def add_offsets( if delay_column is None: delay_column = self.delay_column - metadata: Dict[str, Any] = { + metadata: dict[str, Any] = { "applied": True, } @@ -379,7 +378,7 @@ def extract_delay_stage_parameters( p1_key: str, p2_key: str, t0_key: str, -) -> Tuple: +) -> tuple: """ Read delay stage ranges from hdf5 file @@ -404,18 +403,18 @@ def extract_delay_stage_parameters( def mm_to_ps( - delay_mm: Union[float, np.ndarray], + delay_mm: float | np.ndarray, time0_mm: float, -) -> Union[float, np.ndarray]: +) -> float | np.ndarray: """Converts a delaystage position in mm into a relative delay in picoseconds (double pass). Args: - delay_mm (Union[float, Sequence[float]]): Delay stage position in mm + delay_mm (float | np.ndarray): Delay stage position in mm time0_mm (float): Delay stage position of pump-probe overlap in mm Returns: - Union[float, Sequence[float]]: Relative delay in picoseconds + float | np.ndarray: Relative delay in picoseconds """ delay_ps = (delay_mm - time0_mm) / 0.15 return delay_ps diff --git a/sed/diagnostics.py b/sed/diagnostics.py index 46cbc368..feeeef58 100644 --- a/sed/diagnostics.py +++ b/sed/diagnostics.py @@ -1,8 +1,9 @@ """This module contains diagnostic output functions for the sed module """ -from typing import Sequence -from typing import Tuple +from __future__ import annotations + +from collections.abc import Sequence import bokeh.plotting as pbk import matplotlib.pyplot as plt @@ -54,7 +55,7 @@ def grid_histogram( ncol: int, rvs: Sequence, rvbins: Sequence, - rvranges: Sequence[Tuple[float, float]], + rvranges: Sequence[tuple[float, float]], backend: str = "bokeh", legend: bool = True, histkwds: dict = None, @@ -68,7 +69,7 @@ def grid_histogram( ncol (int): Number of columns in the plot grid. rvs (Sequence): List of names for the random variables (rvs). rvbins (Sequence): Bin values for all random variables. - rvranges (Sequence[Tuple[float, float]]): Value ranges of all random variables. + rvranges (Sequence[tuple[float, float]]): Value ranges of all random variables. backend (str, optional): Backend for making the plot ('matplotlib' or 'bokeh'). Defaults to "bokeh". legend (bool, optional): Option to include a legend in each histogram plot. From 6a4c78a6f99eb26e0d843567f119f79a2a1cb439 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 22:56:23 +0200 Subject: [PATCH 082/300] more type fixes --- sed/calibrator/energy.py | 108 +++++++++++++++++----------------- sed/calibrator/momentum.py | 116 ++++++++++++++++++------------------- sed/core/config.py | 26 ++++----- sed/core/dfops.py | 90 ++++++++++++++-------------- sed/core/logging.py | 2 + sed/core/metadata.py | 49 ++++++++++++++-- sed/io/hdf5.py | 6 +- 7 files changed, 218 insertions(+), 179 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index c52007ec..10e1a64c 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -1,6 +1,8 @@ """sed.calibrator.energy module. Code for energy calibration and correction. Mostly ported from https://github.com/mpes-kit/mpes. """ +from __future__ import annotations + import itertools as it import warnings as wn from collections.abc import Sequence @@ -10,7 +12,6 @@ from typing import Any from typing import cast from typing import Literal -from typing import Union import bokeh.plotting as pbk import dask.dataframe @@ -426,7 +427,7 @@ def apply_func(apply: bool): # noqa: ARG001 def add_ranges( self, - ranges: Union[list[tuple], tuple], + ranges: list[tuple] | tuple, ref_id: int = 0, traces: np.ndarray = None, infer_others: bool = True, @@ -436,7 +437,7 @@ def add_ranges( """Select or extract the equivalent feature ranges (containing the peaks) among all traces. Args: - ranges (Union[List[Tuple], Tuple]): + ranges (list[tuple] | tuple): Collection of feature detection ranges, within which an algorithm (i.e. 1D peak detector) with look for the feature. ref_id (int, optional): Index of the reference trace. Defaults to 0. @@ -776,17 +777,17 @@ def view( # pylint: disable=dangerous-default-value def append_energy_axis( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, tof_column: str = None, energy_column: str = None, calibration: dict = None, verbose: bool = True, **kwds, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the energy axis to the events dataframe. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the energy axis calibration to. tof_column (str, optional): Label of the source column. Defaults to config["dataframe"]["tof_column"]. @@ -805,7 +806,7 @@ def append_energy_axis( NotImplementedError: Raised if an invalid calib_type is found. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: dataframe with added column + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: dataframe with added column and energy calibration metadata dictionary. """ if tof_column is None: @@ -889,15 +890,15 @@ def append_energy_axis( def append_tof_ns_axis( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, tof_column: str = None, tof_ns_column: str = None, **kwds, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Converts the time-of-flight time from steps to time in ns. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to convert. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to convert. tof_column (str, optional): Name of the column containing the time-of-flight steps. Defaults to config["dataframe"]["tof_column"]. tof_ns_column (str, optional): Name of the column to store the @@ -908,8 +909,8 @@ def append_tof_ns_axis( Defaults to config["energy"]["tof_binning"]. Returns: - dask.dataframe.DataFrame: Dataframe with the new columns. - dict: Metadata dictionary. + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with the new columns + and Metadata dictionary. """ binwidth = kwds.pop("binwidth", self.binwidth) binning = kwds.pop("binning", self.binning) @@ -1307,7 +1308,7 @@ def apply_func(apply: bool): def apply_energy_correction( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, tof_column: str = None, new_tof_column: str = None, correction_type: str = None, @@ -1315,11 +1316,11 @@ def apply_energy_correction( correction: dict = None, verbose: bool = True, **kwds, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Apply correction to the time-of-flight (TOF) axis of single-event data. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): The dataframe where + df (pd.DataFrame | dask.dataframe.DataFrame): The dataframe where to apply the energy correction to. tof_column (str, optional): Name of the source column to convert. Defaults to config["dataframe"]["tof_column"]. @@ -1356,7 +1357,7 @@ def apply_energy_correction( asymmetric 2D Lorentz profile, X-direction. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: dataframe with added column + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: dataframe with added column and Energy correction metadata dictionary. """ if correction is None: @@ -1470,16 +1471,16 @@ def align_sector(x): def add_offsets( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame] = None, + df: pd.DataFrame | dask.dataframe.DataFrame = None, offsets: dict[str, Any] = None, constant: float = None, - columns: Union[str, Sequence[str]] = None, - weights: Union[float, Sequence[float]] = None, - preserve_mean: Union[bool, Sequence[bool]] = False, - reductions: Union[str, Sequence[str]] = None, + columns: str | Sequence[str] = None, + weights: float | Sequence[float] = None, + preserve_mean: bool | Sequence[bool] = False, + reductions: str | Sequence[str] = None, energy_column: str = None, verbose: bool = True, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Apply an offset to the energy column by the values of the provided columns. If no parameter is passed to this function, the offset is applied as defined in the @@ -1487,25 +1488,26 @@ def add_offsets( and the offset is applied using the ``dfops.apply_offset_from_columns()`` function. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. offsets (Dict, optional): Dictionary of energy offset parameters. constant (float, optional): The constant to shift the energy axis by. - columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from. - weights (Union[float, Sequence[float]]): weights to apply to the columns. + columns (str | Sequence[str]): Name of the column(s) to apply the shift from. + weights (float | Sequence[float]): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - preserve_mean (bool): Whether to subtract the mean of the column before applying the - shift. Defaults to False. - reductions (str): The reduction to apply to the column. Should be an available method - of dask.dataframe.Series. For example "mean". In this case the function is applied - to the column to generate a single value for the whole dataset. If None, the shift - is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. + preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column + before applying the shift. Defaults to False. + reductions (str | Sequence[str]): The reduction to apply to the column. Should be an + available method of dask.dataframe.Series. For example "mean". In this case the + function is applied to the column to generate a single value for the whole dataset. + If None, the shift is applied per-dataframe-row. Defaults to None. Currently only + "mean" is supported. energy_column (str, optional): Name of the column containing the energy values. verbose (bool, optional): Option to print out diagnostic information. Defaults to True. Returns: - dask.dataframe.DataFrame: Dataframe with the new columns. - dict: Metadata dictionary. + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with the new columns + and Metadata dictionary. """ if offsets is None: offsets = deepcopy(self.offsets) @@ -1663,18 +1665,18 @@ def extract_bias(files: list[str], bias_key: str) -> np.ndarray: def correction_function( - x: Union[float, np.ndarray], - y: Union[float, np.ndarray], + x: float | np.ndarray, + y: float | np.ndarray, correction_type: str, center: tuple[float, float], amplitude: float, **kwds, -) -> Union[float, np.ndarray]: +) -> float | np.ndarray: """Calculate the TOF correction based on the given X/Y coordinates and a model. Args: - x (float): x coordinate - y (float): y coordinate + x (float | np.ndarray): x coordinate + y (float | np.ndarray): y coordinate correction_type (str): type of correction. One of "spherical", "Lorentzian", "Gaussian", or "Lorentzian_asymmetric" center (Tuple[int, int]): center position of the distribution (x,y) @@ -1692,7 +1694,7 @@ def correction_function( asymmetric 2D Lorentz profile, X-direction. Returns: - float: calculated correction value + float | np.ndarray: calculated correction value """ if correction_type == "spherical": try: @@ -2083,13 +2085,13 @@ def peakdetect1d( def fit_energy_calibration( - pos: Union[list[float], np.ndarray], - vals: Union[list[float], np.ndarray], + pos: list[float] | np.ndarray, + vals: list[float] | np.ndarray, binwidth: float, binning: int, ref_id: int = 0, ref_energy: float = None, - t: Union[list[float], np.ndarray] = None, + t: list[float] | np.ndarray = None, energy_scale: str = "kinetic", verbose: bool = True, **kwds, @@ -2099,16 +2101,16 @@ def fit_energy_calibration( function d/(t-t0)**2. Args: - pos (Union[List[float], np.ndarray]): Positions of the spectral landmarks + pos (list[float] | np.ndarray): Positions of the spectral landmarks (e.g. peaks) in the EDCs. - vals (Union[List[float], np.ndarray]): Bias voltage value associated with + vals (list[float] | np.ndarray): Bias voltage value associated with each EDC. binwidth (float): Time width of each original TOF bin in ns. binning (int): Binning factor of the TOF values. ref_id (int, optional): Reference dataset index. Defaults to 0. ref_energy (float, optional): Energy value of the feature in the refence trace (eV). required to output the calibration. Defaults to None. - t (Union[List[float], np.ndarray], optional): Array of TOF values. Required + t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. energy_scale (str, optional): Direction of increasing energy scale. @@ -2219,12 +2221,12 @@ def residual(pars, time, data, binwidth, binning, energy_scale): def poly_energy_calibration( - pos: Union[list[float], np.ndarray], - vals: Union[list[float], np.ndarray], + pos: list[float] | np.ndarray, + vals: list[float] | np.ndarray, order: int = 3, ref_id: int = 0, ref_energy: float = None, - t: Union[list[float], np.ndarray] = None, + t: list[float] | np.ndarray = None, aug: int = 1, method: str = "lstsq", energy_scale: str = "kinetic", @@ -2239,15 +2241,15 @@ def poly_energy_calibration( Args: - pos (Union[List[float], np.ndarray]): Positions of the spectral landmarks + pos (list[float] | np.ndarray): Positions of the spectral landmarks (e.g. peaks) in the EDCs. - vals (Union[List[float], np.ndarray]): Bias voltage value associated with + vals (list[float] | np.ndarray): Bias voltage value associated with each EDC. order (int, optional): Polynomial order of the fitting function. Defaults to 3. ref_id (int, optional): Reference dataset index. Defaults to 0. ref_energy (float, optional): Energy value of the feature in the refence trace (eV). required to output the calibration. Defaults to None. - t (Union[List[float], np.ndarray], optional): Array of TOF values. Required + t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. aug (int, optional): Fitting dimension augmentation (1=no change, 2=double, etc). Defaults to 1. @@ -2370,7 +2372,7 @@ def tof2ev( def tof2evpoly( - poly_a: Union[list[float], np.ndarray], + poly_a: list[float] | np.ndarray, energy_offset: float, t: float, ) -> float: @@ -2378,7 +2380,7 @@ def tof2evpoly( conversion formula. Args: - poly_a (Union[List[float], np.ndarray]): Polynomial coefficients. + poly_a (list[float] | np.ndarray): Polynomial coefficients. energy_offset (float): Energy offset in eV. t (float): TOF value in bin number. diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 19c8d56d..0623ca26 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -1,11 +1,12 @@ """sed.calibrator.momentum module. Code for momentum calibration and distortion correction. Mostly ported from https://github.com/mpes-kit/mpes. """ +from __future__ import annotations + import itertools as it from copy import deepcopy from datetime import datetime from typing import Any -from typing import Union import bokeh.palettes as bp import bokeh.plotting as pbk @@ -37,9 +38,9 @@ class MomentumCorrector: Momentum distortion correction and momentum calibration workflow functions. Args: - data (Union[xr.DataArray, np.ndarray], optional): Multidimensional hypervolume + data (xr.DataArray | np.ndarray, optional): Multidimensional hypervolume containing the data. Defaults to None. - bin_ranges (List[Tuple], optional): Binning ranges of the data volume, if + bin_ranges (list[tuple], optional): Binning ranges of the data volume, if provided as np.ndarray. Defaults to None. rotsym (int, optional): Rotational symmetry of the data. Defaults to 6. config (dict, optional): Config dictionary. Defaults to None. @@ -47,7 +48,7 @@ class MomentumCorrector: def __init__( self, - data: Union[xr.DataArray, np.ndarray] = None, + data: xr.DataArray | np.ndarray = None, bin_ranges: list[tuple] = None, rotsym: int = 6, config: dict = None, @@ -55,9 +56,9 @@ def __init__( """Constructor of the MomentumCorrector class. Args: - data (Union[xr.DataArray, np.ndarray], optional): Multidimensional + data (xr.DataArray | np.ndarray, optional): Multidimensional hypervolume containing the data. Defaults to None. - bin_ranges (List[Tuple], optional): Binning ranges of the data volume, + bin_ranges (list[tuple], optional): Binning ranges of the data volume, if provided as np.ndarray. Defaults to None. rotsym (int, optional): Rotational symmetry of the data. Defaults to 6. config (dict, optional): Config dictionary. Defaults to None. @@ -150,15 +151,15 @@ def symscores(self) -> dict: def load_data( self, - data: Union[xr.DataArray, np.ndarray], + data: xr.DataArray | np.ndarray, bin_ranges: list[tuple] = None, ): """Load binned data into the momentum calibrator class Args: - data (Union[xr.DataArray, np.ndarray]): + data (xr.DataArray | np.ndarray): 2D or 3D data array, either as np.ndarray or xr.DataArray. - bin_ranges (List[Tuple], optional): + bin_ranges (list[tuple], optional): Binning ranges. Needs to be provided in case the data are given as np.ndarray. Otherwise, they are determined from the coords of the xr.DataArray. Defaults to None. @@ -282,13 +283,13 @@ def apply_fun(apply: bool): # noqa: ARG001 def select_slice( self, - selector: Union[slice, list[int], int], + selector: slice | list[int] | int, axis: int = 2, ): """Select (hyper)slice from a (hyper)volume. Args: - selector (Union[slice, List[int], int]): + selector (slice | list[int] | int): Selector along the specified axis to extract the slice (image). Use the construct slice(start, stop, step) to select a range of images and sum them. Use an integer to specify only a particular slice. @@ -589,7 +590,7 @@ def spline_warp_estimate( use_center: bool = None, fixed_center: bool = True, interp_order: int = 1, - ascale: Union[float, list, tuple, np.ndarray] = None, + ascale: float | list | tuple | np.ndarray = None, verbose: bool = True, **kwds, ) -> np.ndarray: @@ -607,13 +608,13 @@ def spline_warp_estimate( interp_order (int, optional): Order of interpolation (see ``scipy.ndimage.map_coordinates()``). Defaults to 1. - ascale: (Union[float, np.ndarray], optional): Scale parameter determining a realtive - scale for each symmetry feature. If provided as single float, rotsym has to be 4. - This parameter describes the relative scaling between the two orthogonal symmetry - directions (for an orthorhombic system). This requires the correction points to be - located along the principal axes (X/Y points of the Brillouin zone). Otherwise, an - array with ``rotsym`` elements is expected, containing relative scales for each - feature. Defaults to an array of equal scales. + ascale: (float | list | tuple | np.ndarray, optional): Scale parameter determining a + realtive scale for each symmetry feature. If provided as single float, rotsym has + to be 4. This parameter describes the relative scaling between the two orthogonal + symmetry directions (for an orthorhombic system). This requires the correction + points to be located along the principal axes (X/Y points of the Brillouin zone). + Otherwise, an array with ``rotsym`` elements is expected, containing relative + scales for each feature. Defaults to an array of equal scales. verbose (bool, optional): Option to report the used landmarks for correction. Defaults to True. **kwds: keyword arguments: @@ -1276,7 +1277,7 @@ def view( # pylint: disable=dangerous-default-value origin (str, optional): Figure origin specification ('lower' or 'upper'). Defaults to "lower". cmap (str, optional): Colormap specification. Defaults to "terrain_r". - figsize (Tuple[int, int], optional): Figure size. Defaults to (4, 4). + figsize (tuple[int, int], optional): Figure size. Defaults to (4, 4). points (dict, optional): Points for annotation. Defaults to None. annotated (bool, optional): Option to add annotation. Defaults to False. backend (str, optional): Visualization backend specification. Defaults to @@ -1293,7 +1294,7 @@ def view( # pylint: disable=dangerous-default-value self.pcent. Defaults to False. crosshair (bool, optional): Display option to plot circles around center self.pcent. Works only in bokeh backend. Defaults to False. - crosshair_radii (List[int], optional): Pixel radii of circles to plot when + crosshair_radii (list[int], optional): Pixel radii of circles to plot when crosshair option is activated. Defaults to [50, 100, 150]. crosshair_thickness (int, optional): Thickness of crosshair circles. Defaults to 1. @@ -1404,11 +1405,11 @@ def view( # pylint: disable=dangerous-default-value def select_k_range( self, - point_a: Union[np.ndarray, list[int]] = None, - point_b: Union[np.ndarray, list[int]] = None, + point_a: np.ndarray | list[int] = None, + point_b: np.ndarray | list[int] = None, k_distance: float = None, - k_coord_a: Union[np.ndarray, list[float]] = None, - k_coord_b: Union[np.ndarray, list[float]] = np.array([0.0, 0.0]), + k_coord_a: np.ndarray | list[float] = None, + k_coord_b: np.ndarray | list[float] = np.array([0.0, 0.0]), equiscale: bool = True, apply: bool = False, ): @@ -1419,16 +1420,16 @@ def select_k_range( specifications of point coordinates. Args: - point_a (Union[np.ndarray, List[int]], optional): Pixel coordinates of the + point_a (np.ndarray | list[int], optional): Pixel coordinates of the symmetry point a. - point_b (Union[np.ndarray, List[int]], optional): Pixel coordinates of the + point_b (np.ndarray | list[int], optional): Pixel coordinates of the symmetry point b. Defaults to the center pixel of the image, defined by config["momentum"]["center_pixel"]. k_distance (float, optional): The known momentum space distance between the two symmetry points. - k_coord_a (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_a (np.ndarray | list[float], optional): Momentum coordinate of the symmetry points a. Only valid if equiscale=False. - k_coord_b (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_b (np.ndarray | list[float], optional): Momentum coordinate of the symmetry points b. Only valid if equiscale=False. Defaults to the k-space center np.array([0.0, 0.0]). equiscale (bool, optional): Option to adopt equal scale along both the x @@ -1555,11 +1556,11 @@ def apply_func(apply: bool): # noqa: ARG001 def calibrate( self, - point_a: Union[np.ndarray, list[int]], - point_b: Union[np.ndarray, list[int]], + point_a: np.ndarray | list[int], + point_b: np.ndarray | list[int], k_distance: float = None, - k_coord_a: Union[np.ndarray, list[float]] = None, - k_coord_b: Union[np.ndarray, list[float]] = np.array([0.0, 0.0]), + k_coord_a: np.ndarray | list[float] = None, + k_coord_b: np.ndarray | list[float] = np.array([0.0, 0.0]), equiscale: bool = True, image: np.ndarray = None, ) -> dict: @@ -1570,16 +1571,16 @@ def calibrate( of point coordinates. Args: - point_a (Union[np.ndarray, List[int]], optional): Pixel coordinates of the + point_a (np.ndarray | list[int], optional): Pixel coordinates of the symmetry point a. - point_b (Union[np.ndarray, List[int]], optional): Pixel coordinates of the + point_b (np.ndarray | list[int], optional): Pixel coordinates of the symmetry point b. Defaults to the center pixel of the image, defined by config["momentum"]["center_pixel"]. k_distance (float, optional): The known momentum space distance between the two symmetry points. - k_coord_a (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_a (np.ndarray | list[float], optional): Momentum coordinate of the symmetry points a. Only valid if equiscale=False. - k_coord_b (Union[np.ndarray, List[float]], optional): Momentum coordinate + k_coord_b (np.ndarray | list[float], optional): Momentum coordinate of the symmetry points b. Only valid if equiscale=False. Defaults to the k-space center np.array([0.0, 0.0]). equiscale (bool, optional): Option to adopt equal scale along both the x @@ -1672,19 +1673,19 @@ def calibrate( def apply_corrections( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, x_column: str = None, y_column: str = None, new_x_column: str = None, new_y_column: str = None, verbose: bool = True, **kwds, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and replace the X and Y values with their distortion-corrected version. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to apply + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the distotion correction to. x_column (str, optional): Label of the 'X' column before momentum distortion correction. Defaults to config["momentum"]["x_column"]. @@ -1707,7 +1708,7 @@ def apply_corrections( Additional keyword arguments are passed to ``apply_dfield``. Returns: - Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: Dataframe with + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with added columns and momentum correction metadata dictionary. """ if x_column is None: @@ -1831,18 +1832,18 @@ def gather_correction_metadata(self) -> dict: def append_k_axis( self, - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, x_column: str = None, y_column: str = None, new_x_column: str = None, new_y_column: str = None, calibration: dict = None, **kwds, - ) -> tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: + ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the k axis coordinates (kx, ky) to the events dataframe. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to apply the + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the distotion correction to. x_column (str, optional): Label of the source 'X' column. Defaults to config["momentum"]["corrected_x_column"] or @@ -1860,7 +1861,7 @@ def append_k_axis( to the calibration dictionary. Returns: - Tuple[Union[pd.DataFrame, dask.dataframe.DataFrame], dict]: Dataframe with + tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with added columns and momentum calibration metadata dictionary. """ if x_column is None: @@ -1962,13 +1963,13 @@ def cm2palette(cmap_name: str) -> list: def dictmerge( main_dict: dict, - other_entries: Union[list[dict], tuple[dict], dict], + other_entries: list[dict] | tuple[dict] | dict, ) -> dict: """Merge a dictionary with other dictionaries. Args: main_dict (dict): Main dictionary. - other_entries (Union[List[dict], Tuple[dict], dict]): + other_entries (list[dict] | tuple[dict] | dict): Other dictionary or composite dictionarized elements. Returns: @@ -1976,10 +1977,7 @@ def dictmerge( """ if isinstance( other_entries, - ( - list, - tuple, - ), + (list, tuple), ): # Merge main_dict with a list or tuple of dictionaries for oth in other_entries: main_dict = {**main_dict, **oth} @@ -2018,7 +2016,7 @@ def detector_coordiantes_2_k_koordinates( c_step (float): Column stepping factor. Returns: - Tuple[float, float]: Converted momentum space row/column coordinates. + tuple[float, float]: Converted momentum space row/column coordinates. """ r_det0 = r_start + r_step * r_center c_det0 = c_start + c_step * c_center @@ -2029,18 +2027,18 @@ def detector_coordiantes_2_k_koordinates( def apply_dfield( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, dfield: np.ndarray, x_column: str, y_column: str, new_x_column: str, new_y_column: str, detector_ranges: list[tuple], -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> pd.DataFrame | dask.dataframe.DataFrame: """Application of the inverse displacement-field to the dataframe coordinates. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to apply the + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the distotion correction to. dfield (np.ndarray): The distortion correction field. 3D matrix, with column and row distortion fields stacked along the first dimension. @@ -2048,11 +2046,11 @@ def apply_dfield( y_column (str): Label of the 'Y' source column. new_x_column (str): Label of the 'X' destination column. new_y_column (str): Label of the 'Y' destination column. - detector_ranges (List[Tuple]): tuple of pixel ranges of the detector x/y + detector_ranges (list[tuple]): tuple of pixel ranges of the detector x/y coordinates Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: dataframe with added columns + pd.DataFrame | dask.dataframe.DataFrame: dataframe with added columns """ x = df[x_column] y = df[y_column] @@ -2080,8 +2078,8 @@ def generate_inverse_dfield( Args: rdeform_field (np.ndarray): Row-wise deformation field. cdeform_field (np.ndarray): Column-wise deformation field. - bin_ranges (List[Tuple]): Detector ranges of the binned coordinates. - detector_ranges (List[Tuple]): Ranges of detector coordinates to interpolate to. + bin_ranges (list[tuple]): Detector ranges of the binned coordinates. + detector_ranges (list[tuple]): Ranges of detector coordinates to interpolate to. Returns: np.ndarray: The calculated inverse deformation field (row/column) diff --git a/sed/core/config.py b/sed/core/config.py index 2de09240..868d2f06 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -1,12 +1,13 @@ """This module contains a config library for loading yaml/json files into dicts """ +from __future__ import annotations + import copy import json import os import platform from importlib.util import find_spec from pathlib import Path -from typing import Union import yaml from platformdirs import user_config_path @@ -17,14 +18,11 @@ def parse_config( - config: Union[dict, str] = None, - folder_config: Union[dict, str] = None, - user_config: Union[dict, str] = None, - system_config: Union[dict, str] = None, - default_config: Union[ - dict, - str, - ] = f"{package_dir}/config/default.yaml", + config: dict | str = None, + folder_config: dict | str = None, + user_config: dict | str = None, + system_config: dict | str = None, + default_config: (dict | str) = f"{package_dir}/config/default.yaml", verbose: bool = True, ) -> dict: """Load the config dictionary from a file, or pass the provided config dictionary. @@ -34,21 +32,21 @@ def parse_config( can be also passed as optional arguments (file path strings or dictionaries). Args: - config (Union[dict, str], optional): config dictionary or file path. + config (dict | str, optional): config dictionary or file path. Files can be *json* or *yaml*. Defaults to None. - folder_config (Union[ dict, str, ], optional): working-folder-based config dictionary + folder_config (dict | str, optional): working-folder-based config dictionary or file path. The loaded dictionary is completed with the folder-based values, taking preference over user, system and default values. Defaults to the file "sed_config.yaml" in the current working directory. - user_config (Union[ dict, str, ], optional): user-based config dictionary + user_config (dict | str, optional): user-based config dictionary or file path. The loaded dictionary is completed with the user-based values, taking preference over system and default values. Defaults to the file ".sed/config.yaml" in the current user's home directory. - system_config (Union[ dict, str, ], optional): system-wide config dictionary + system_config (dict | str, optional): system-wide config dictionary or file path. The loaded dictionary is completed with the system-wide values, taking preference over default values. Defaults to the file "/etc/sed/config.yaml" on linux, and "%ALLUSERSPROFILE%/sed/config.yaml" on windows. - default_config (Union[ dict, str, ], optional): default config dictionary + default_config (dict | str, optional): default config dictionary or file path. The loaded dictionary is completed with the default values. Defaults to *package_dir*/config/default.yaml". verbose (bool, optional): Option to report loaded config files. Defaults to True. diff --git a/sed/core/dfops.py b/sed/core/dfops.py index 7127f7f0..b1f8779c 100644 --- a/sed/core/dfops.py +++ b/sed/core/dfops.py @@ -3,9 +3,10 @@ """ # Note: some of the functions presented here were # inspired by https://github.com/mpes-kit/mpes +from __future__ import annotations + +from collections.abc import Sequence from typing import Callable -from typing import Sequence -from typing import Union import dask.dataframe import numpy as np @@ -14,21 +15,21 @@ def apply_jitter( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], - cols: Union[str, Sequence[str]], - cols_jittered: Union[str, Sequence[str]] = None, - amps: Union[float, Sequence[float]] = 0.5, + df: pd.DataFrame | dask.dataframe.DataFrame, + cols: str | Sequence[str], + cols_jittered: str | Sequence[str] = None, + amps: float | Sequence[float] = 0.5, jitter_type: str = "uniform", -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> pd.DataFrame | dask.dataframe.DataFrame: """Add jittering to one or more dataframe columns. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to add + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to add noise/jittering to. - cols (Union[str, Sequence[str]]): Names of the columns to add jittering to. - cols_jittered (Union[str, Sequence[str]], optional): Names of the columns + cols (str | Sequence[str]): Names of the columns to add jittering to. + cols_jittered (str | Sequence[str], optional): Names of the columns with added jitter. Defaults to None. - amps (Union[float, Sequence[float]], optional): Amplitude scalings for the + amps (float | Sequence[float], optional): Amplitude scalings for the jittering noise. If one number is given, the same is used for all axes. For normal noise, the added noise will have sdev [-amp, +amp], for uniform noise it will cover the interval [-amp, +amp]. @@ -37,7 +38,7 @@ def apply_jitter( distributed noise. Defaults to "uniform". Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: dataframe with added columns. + pd.DataFrame | dask.dataframe.DataFrame: dataframe with added columns. """ assert cols is not None, "cols needs to be provided!" assert jitter_type in ( @@ -71,17 +72,17 @@ def apply_jitter( def drop_column( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], - column_name: Union[str, Sequence[str]], -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: + df: pd.DataFrame | dask.dataframe.DataFrame, + column_name: str | Sequence[str], +) -> pd.DataFrame | dask.dataframe.DataFrame: """Delete columns. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. - column_name (Union[str, Sequence[str]])): List of column names to be dropped. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. + column_name (str | Sequence[str]): List of column names to be dropped. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: Dataframe with dropped columns. + pd.DataFrame | dask.dataframe.DataFrame: Dataframe with dropped columns. """ out_df = df.drop(column_name, axis=1) @@ -89,15 +90,15 @@ def drop_column( def apply_filter( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, col: str, lower_bound: float = -np.inf, upper_bound: float = np.inf, -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> pd.DataFrame | dask.dataframe.DataFrame: """Application of bound filters to a specified column (can be used consecutively). Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. col (str): Name of the column to filter. Passing "index" for col will filter on the index in each dataframe partition. lower_bound (float, optional): The lower bound used in the filtering. @@ -106,7 +107,7 @@ def apply_filter( Defaults to np.inf. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: The filtered dataframe. + pd.DataFrame | dask.dataframe.DataFrame: The filtered dataframe. """ df = df.copy() if col == "index": @@ -132,14 +133,14 @@ def add_time_stamped_data( timestamps in the dataframe. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (dask.dataframe.DataFrame): Dataframe to use. time_stamps (np.ndarray): Time stamps of the values to add data (np.ndarray): Values corresponding at the time stamps in time_stamps dest_column (str): destination column name time_stamp_column (str): Time stamp column name Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: Dataframe with added column + dask.dataframe.DataFrame: Dataframe with added column """ if time_stamp_column not in df.columns: raise ValueError(f"{time_stamp_column} not found in dataframe!") @@ -163,23 +164,23 @@ def interpolate_timestamps( def map_columns_2d( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, map_2d: Callable, x_column: str, y_column: str, **kwds, -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> pd.DataFrame | dask.dataframe.DataFrame: """Apply a 2-dimensional mapping simultaneously to two dimensions. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. map_2d (Callable): 2D mapping function. x_column (str): The X column of the dataframe to apply mapping to. y_column (str): The Y column of the dataframe to apply mapping to. **kwds: Additional arguments for the 2D mapping function. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: Dataframe with mapped columns. + pd.DataFrame | dask.dataframe.DataFrame: Dataframe with mapped columns. """ new_x_column = kwds.pop("new_x_column", x_column) new_y_column = kwds.pop("new_y_column", y_column) @@ -196,7 +197,7 @@ def map_columns_2d( def forward_fill_lazy( df: dask.dataframe.DataFrame, columns: Sequence[str] = None, - before: Union[str, int] = "max", + before: str | int = "max", compute_lengths: bool = False, iterations: int = 2, ) -> dask.dataframe.DataFrame: @@ -210,8 +211,8 @@ def forward_fill_lazy( Args: df (dask.dataframe.DataFrame): The dataframe to forward fill. - columns (list): The columns to forward fill. If None, fills all columns - before (int, str, optional): The number of rows to include before the current partition. + columns (list, optional): The columns to forward fill. If None, fills all columns + before (str | int, optional): The number of rows to include before the current partition. if 'max' it takes as much as possible from the previous partition, which is the size of the smallest partition in the dataframe. Defaults to 'max'. compute_lengths (bool, optional): Whether to compute the length of each partition @@ -258,7 +259,7 @@ def forward_fill_partition(df): def backward_fill_lazy( df: dask.dataframe.DataFrame, columns: Sequence[str] = None, - after: Union[str, int] = "max", + after: str | int = "max", compute_lengths: bool = False, iterations: int = 1, ) -> dask.dataframe.DataFrame: @@ -270,8 +271,8 @@ def backward_fill_lazy( Args: df (dask.dataframe.DataFrame): The dataframe to forward fill. - columns (list): The columns to forward fill. If None, fills all columns - after (int, str, optional): The number of rows to include after the current partition. + columns (list, optional): The columns to forward fill. If None, fills all columns + after (str | int, optional): The number of rows to include after the current partition. if 'max' it takes as much as possible from the previous partition, which is the size of the smallest partition in the dataframe. Defaults to 'max'. compute_lengths (bool, optional): Whether to compute the length of each partition @@ -318,10 +319,10 @@ def backward_fill_partition(df): def offset_by_other_columns( df: dask.dataframe.DataFrame, target_column: str, - offset_columns: Union[str, Sequence[str]], - weights: Union[float, Sequence[float]], - reductions: Union[str, Sequence[str]] = None, - preserve_mean: Union[bool, Sequence[bool]] = False, + offset_columns: str | Sequence[str], + weights: float | Sequence[float], + reductions: str | Sequence[str] = None, + preserve_mean: bool | Sequence[bool] = False, inplace: bool = True, rename: str = None, ) -> dask.dataframe.DataFrame: @@ -330,12 +331,13 @@ def offset_by_other_columns( Args: df (dask.dataframe.DataFrame): Dataframe to use. Currently supports only dask dataframes. target_column (str): Name of the column to apply the offset to. - offset_columns (str): Name of the column(s) to use for the offset. - weights (flot): weights to apply on each column before adding. Used also for changing sign. - reductions (str, optional): Reduction function to use for the offset. Defaults to "mean". - Currently, only mean is supported. - preserve_mean (bool, optional): Whether to subtract the mean of the offset column. - Defaults to False. If a list is given, it must have the same length as + offset_columns (str | Sequence[str]): Name of the column(s) to use for the offset. + weights (float | Sequence[float]): weights to apply on each column before adding. Used also + for changing sign. + reductions (str | Sequence[str], optional): Reduction function to use for the offset. + Defaults to "mean". Currently, only mean is supported. + preserve_mean (bool | Sequence[bool], optional): Whether to subtract the mean of the offset + column. Defaults to False. If a list is given, it must have the same length as offset_columns. Otherwise the value passed is used for all columns. inplace (bool, optional): Whether to apply the offset inplace. If false, the new column will have the name provided by rename, or has the same name as diff --git a/sed/core/logging.py b/sed/core/logging.py index c55a8bbb..8c4742a7 100644 --- a/sed/core/logging.py +++ b/sed/core/logging.py @@ -4,6 +4,8 @@ log files are stored in a user-specific log directory. """ +from __future__ import annotations + import logging import os import sys diff --git a/sed/core/metadata.py b/sed/core/metadata.py index 155bdfce..77531987 100644 --- a/sed/core/metadata.py +++ b/sed/core/metadata.py @@ -1,27 +1,59 @@ """This is a metadata handler class from the sed package """ +from __future__ import annotations + import json from copy import deepcopy from typing import Any -from typing import Dict from sed.core.config import complete_dictionary class MetaHandler: """This class provides methods to manipulate metadata dictionaries, - and give a nice representation of them.""" + and give a nice representation of them. + + Args: + meta (dict, optional): Pre-existing metadata dict. Defaults to None. + """ - def __init__(self, meta: Dict = None) -> None: + def __init__(self, meta: dict = None) -> None: + """Constructor. + + Args: + meta (dict, optional): Pre-existing metadata dict. Defaults to None. + """ self._m = deepcopy(meta) if meta is not None else {} - def __getitem__(self, val: Any) -> None: + def __getitem__(self, val: Any) -> Any: + """Function for getting a value + + Args: + val (Any): Metadata category key + + Returns: + Any: The metadata category entry. + """ return self._m[val] def __repr__(self) -> str: + """String representation function as json + + Returns: + str: Summary string. + """ return json.dumps(self._m, default=str, indent=4) - def _format_attributes(self, attributes, indent=0): + def _format_attributes(self, attributes: dict, indent: int = 0) -> str: + """Function to summarize a dictionary as html + + Args: + attributes (dict): dictionary to summarize + indent (int, optional): Indentation value. Defaults to 0. + + Returns: + str: Generated html summary. + """ INDENT_FACTOR = 20 html = "" for key, value in attributes.items(): @@ -42,11 +74,16 @@ def _format_attributes(self, attributes, indent=0): return html def _repr_html_(self) -> str: + """Summary function as html + + Returns: + str: Generated html summary + """ html = self._format_attributes(self._m) return html @property - def metadata(self) -> Dict: + def metadata(self) -> dict: """Property returning the metadata dict. Returns: dict: Dictionary of metadata. diff --git a/sed/io/hdf5.py b/sed/io/hdf5.py index 34ab0016..9365c61d 100644 --- a/sed/io/hdf5.py +++ b/sed/io/hdf5.py @@ -1,7 +1,7 @@ """This module contains hdf5 file input/output functions for the sed.io module """ -from typing import Union +from __future__ import annotations import h5py import numpy as np @@ -53,12 +53,12 @@ def recursive_write_metadata(h5group: h5py.Group, node: dict): def recursive_parse_metadata( - node: Union[h5py.Group, h5py.Dataset], + node: h5py.Group | h5py.Dataset, ) -> dict: """Recurses through an hdf5 file, and parse it into a dictionary. Args: - node (Union[h5py.Group, h5py.Dataset]): hdf5 group or dataset to parse into + node (h5py.Group | h5py.Dataset): hdf5 group or dataset to parse into dictionary. Returns: From 843b16bac77957a8a7172012ae183d28f786babc Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 23:21:02 +0200 Subject: [PATCH 083/300] more typing fixes --- sed/io/nexus.py | 9 +++-- sed/io/tiff.py | 19 ++++----- sed/loader/base/loader.py | 47 +++++++++++----------- sed/loader/flash/loader.py | 73 ++++++++++++++++++---------------- sed/loader/flash/metadata.py | 29 ++++++++------ sed/loader/generic/loader.py | 35 ++++++++-------- sed/loader/loader_interface.py | 7 ++-- sed/loader/mirrorutil.py | 7 ++-- sed/loader/mpes/loader.py | 62 ++++++++++++++--------------- sed/loader/sxp/loader.py | 73 ++++++++++++++++++---------------- sed/loader/utils.py | 24 +++++------ 11 files changed, 198 insertions(+), 187 deletions(-) diff --git a/sed/io/nexus.py b/sed/io/nexus.py index 99ca139d..c0b10185 100644 --- a/sed/io/nexus.py +++ b/sed/io/nexus.py @@ -3,8 +3,9 @@ For details, see https://github.com/nomad-coe/nomad-parser-nexus """ -from typing import Sequence -from typing import Union +from __future__ import annotations + +from collections.abc import Sequence import xarray as xr from pynxtools.dataconverter.convert import convert @@ -15,7 +16,7 @@ def to_nexus( faddr: str, reader: str, definition: str, - input_files: Union[str, Sequence[str]], + input_files: str | Sequence[str], **kwds, ): """Saves the x-array provided to a NeXus file at faddr, using the provided reader, @@ -27,7 +28,7 @@ def to_nexus( faddr (str): The file path to save to. reader (str): The name of the NeXus reader to use. definition (str): The NeXus definiton to use. - config_file (str): The file path to the configuration file to use. + input_files (str | Sequence[str]): The file path or paths to the additional files to use. **kwds: Keyword arguments for ``nexusutils.dataconverter.convert``. """ diff --git a/sed/io/tiff.py b/sed/io/tiff.py index 26f0f23f..4de1a42c 100644 --- a/sed/io/tiff.py +++ b/sed/io/tiff.py @@ -1,9 +1,10 @@ """This module contains tiff file input/output functions for the sed.io module """ +from __future__ import annotations + +from collections.abc import Sequence from pathlib import Path -from typing import Sequence -from typing import Union import numpy as np import tifffile @@ -37,20 +38,20 @@ def to_tiff( - data: Union[xr.DataArray, np.ndarray], - faddr: Union[Path, str], + data: xr.DataArray | np.ndarray, + faddr: Path | str, alias_dict: dict = None, ): """Save an array as a .tiff stack compatible with ImageJ Args: - data (Union[xr.DataArray, np.ndarray]): data to be saved. If a np.ndarray, + data (xr.DataArray | np.ndarray): data to be saved. If a np.ndarray, the order is retained. If it is an xarray.DataArray, the order is inferred from axis_dict instead. ImageJ likes tiff files with axis order as TZCYXS. Therefore, best axis order in input should be: Time, Energy, posY, posX. The channels 'C' and 'S' are automatically added and can be ignored. - faddr (Union[Path, str]): full path and name of file to save. + faddr Path | str): full path and name of file to save. alias_dict (dict, optional): name pairs for correct axis ordering. Keys should be any of T,Z,C,Y,X,S. The Corresponding value should be a dimension of the xarray or the dimension number if a numpy array. This is used to sort the @@ -63,7 +64,7 @@ def to_tiff( NotImplementedError: if data is not 2,3 or 4 dimensional TypeError: if data is not a np.ndarray or an xarray.DataArray """ - out: Union[np.ndarray, xr.DataArray] = None + out: np.ndarray | xr.DataArray = None if isinstance(data, np.ndarray): # TODO: add sorting by dictionary keys dim_expansions = {2: [0, 1, 2, 5], 3: [0, 2, 5], 4: [2, 5]} @@ -172,7 +173,7 @@ def _fill_missing_dims(dims: Sequence, alias_dict: dict = None) -> list: def load_tiff( - faddr: Union[str, Path], + faddr: str | Path, coords: dict = None, dims: Sequence = None, attrs: dict = None, @@ -184,7 +185,7 @@ def load_tiff( only as np.ndarray. Args: - faddr (Union[str, Path]): Path to file to load. + faddr (str | Path): Path to file to load. coords (dict, optional): The axes describing the data, following the tiff stack order. Defaults to None. dims (Sequence, optional): the order of the coordinates provided, considering diff --git a/sed/loader/base/loader.py b/sed/loader/base/loader.py index 880be88d..61bab069 100644 --- a/sed/loader/base/loader.py +++ b/sed/loader/base/loader.py @@ -1,14 +1,13 @@ -"""The abstract class off of which to implement loaders.""" +"""The abstract class off of which to implement loaders. +""" +from __future__ import annotations + import os from abc import ABC from abc import abstractmethod +from collections.abc import Sequence from copy import deepcopy from typing import Any -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as ddf import numpy as np @@ -32,7 +31,7 @@ class BaseLoader(ABC): __name__ = "BaseLoader" - supported_file_types: List[str] = [] + supported_file_types: list[str] = [] def __init__( self, @@ -40,31 +39,31 @@ def __init__( ): self._config = config if config is not None else {} - self.files: List[str] = [] - self.runs: List[str] = [] - self.metadata: Dict[Any, Any] = {} + self.files: list[str] = [] + self.runs: list[str] = [] + self.metadata: dict[Any, Any] = {} @abstractmethod def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = None, metadata: dict = None, collect_metadata: bool = False, **kwds, - ) -> Tuple[ddf.DataFrame, ddf.DataFrame, dict]: + ) -> tuple[ddf.DataFrame, ddf.DataFrame, dict]: """Reads data from given files, folder, or runs and returns a dask dataframe and corresponding metadata. Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. + files (str | Sequence[str], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files + folders (str | Sequence[str], optional): Path to folder(s) where files are stored. Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding + runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes precendence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File type to read ('parquet', 'json', 'csv', etc). @@ -77,7 +76,7 @@ def read_dataframe( **kwds: keyword arguments. See description in respective loader. Returns: - Tuple[ddf.DataFrame, dict]: Dask dataframe, timed dataframe and metadata + tuple[ddf.DataFrame, ddf.DataFrame, dict]: Dask dataframe, timed dataframe and metadata read from specified files. """ @@ -129,21 +128,21 @@ def read_dataframe( def get_files_from_run_id( self, run_id: str, - folders: Union[str, Sequence[str]] = None, + folders: str | Sequence[str] = None, extension: str = None, **kwds, - ) -> List[str]: + ) -> list[str]: """Locate the files for a given run identifier. Args: run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to None. extension (str, optional): The file extension. Defaults to None. kwds: Keyword arguments Return: - List[str]: List of files for the given run. + list[str]: List of files for the given run. """ raise NotImplementedError @@ -152,7 +151,7 @@ def get_count_rate( self, fids: Sequence[int] = None, **kwds, - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Create count rate data for the files specified in ``fids``. Args: @@ -161,7 +160,7 @@ def get_count_rate( kwds: Keyword arguments Return: - Tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds + tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds into the scan. """ return None, None diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 8f9198b6..dfb82088 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -7,13 +7,12 @@ This can then be saved as a parquet for out-of-sed processing and reread back to access other sed funtionality. """ +from __future__ import annotations + import time +from collections.abc import Sequence from functools import reduce from pathlib import Path -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as dd import h5py @@ -49,14 +48,14 @@ def __init__(self, config: dict) -> None: self.multi_index = ["trainId", "pulseId", "electronId"] self.index_per_electron: MultiIndex = None self.index_per_pulse: MultiIndex = None - self.failed_files_error: List[str] = [] + self.failed_files_error: list[str] = [] - def initialize_paths(self) -> Tuple[List[Path], Path]: + def initialize_paths(self) -> tuple[list[Path], Path]: """ Initializes the paths based on the configuration. Returns: - Tuple[List[Path], Path]: A tuple containing a list of raw data directories + tuple[list[Path], Path]: A tuple containing a list of raw data directories paths and the parquet data directory path. Raises: @@ -114,23 +113,23 @@ def initialize_paths(self) -> Tuple[List[Path], Path]: def get_files_from_run_id( self, run_id: str, - folders: Union[str, Sequence[str]] = None, + folders: str | Sequence[str] = None, extension: str = "h5", **kwds, - ) -> List[str]: + ) -> list[str]: """Returns a list of filenames for a given run located in the specified directory for the specified data acquisition (daq). Args: run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"]. extension (str, optional): The file extension. Defaults to "h5". kwds: Keyword arguments: - daq (str): The data acquisition identifier. Returns: - List[str]: A list of path strings representing the collected file names. + list[str]: A list of path strings representing the collected file names. Raises: FileNotFoundError: If no files are found for the given run in the directory. @@ -149,7 +148,7 @@ def get_files_from_run_id( # Generate the file patterns to search for in the directory file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension - files: List[Path] = [] + files: list[Path] = [] # Use pathlib to search for matching files in each directory for folder in folders: files.extend( @@ -169,24 +168,24 @@ def get_files_from_run_id( return [str(file.resolve()) for file in files] @property - def available_channels(self) -> List: + def available_channels(self) -> list: """Returns the channel names that are available for use, excluding pulseId, defined by the json file""" available_channels = list(self._config["dataframe"]["channels"].keys()) available_channels.remove("pulseId") return available_channels - def get_channels(self, formats: Union[str, List[str]] = "", index: bool = False) -> List[str]: + def get_channels(self, formats: str | list[str] = "", index: bool = False) -> list[str]: """ Returns a list of channels associated with the specified format(s). Args: - formats (Union[str, List[str]]): The desired format(s) - ('per_pulse', 'per_electron', 'per_train', 'all'). + formats (str | list[str]): The desired format(s) + ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multi_index. Returns: - List[str]: A list of channels with the specified format(s). + list[str]: A list of channels with the specified format(s). """ # If 'formats' is a single string, convert it to a list for uniform processing. if isinstance(formats, str): @@ -311,7 +310,7 @@ def create_numpy_array_per_channel( self, h5_file: h5py.File, channel: str, - ) -> Tuple[Series, np.ndarray]: + ) -> tuple[Series, np.ndarray]: """ Returns a numpy array for a given channel name for a given file. @@ -320,7 +319,7 @@ def create_numpy_array_per_channel( channel (str): The name of the channel. Returns: - Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array + tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array for the channel's data. """ @@ -467,7 +466,7 @@ def create_dataframe_per_channel( self, h5_file: h5py.File, channel: str, - ) -> Union[Series, DataFrame]: + ) -> Series | DataFrame: """ Returns a pandas DataFrame for a given channel name from a given file. @@ -480,7 +479,7 @@ def create_dataframe_per_channel( channel (str): The name of the channel. Returns: - Union[Series, DataFrame]: A pandas Series or DataFrame representing the channel's data. + Series | DataFrame: A pandas Series or DataFrame representing the channel's data. Raises: ValueError: If the channel has an undefined format. @@ -614,7 +613,7 @@ def create_dataframe_per_file( df = split_dld_time_from_sector_id(df, config=self._config) return df - def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, Exception]: + def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> bool | Exception: """ Converts an HDF5 file to Parquet format to create a buffer file. @@ -625,6 +624,9 @@ def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, E h5_path (Path): Path to the input HDF5 file. parquet_path (Path): Path to the output Parquet file. + Returns: + bool | Exception: Collected exceptions, if any. + Raises: ValueError: If an error occurs during the conversion process. @@ -645,7 +647,7 @@ def buffer_file_handler( data_parquet_dir: Path, detector: str, force_recreate: bool, - ) -> Tuple[List[Path], List, List]: + ) -> tuple[list[Path], list, list]: """ Handles the conversion of buffer files (h5 to parquet) and returns the filenames. @@ -655,7 +657,7 @@ def buffer_file_handler( force_recreate (bool): Forces recreation of buffer files Returns: - Tuple[List[Path], List, List]: Three lists, one for + tuple[list[Path], list, list]: Three lists, one for parquet file paths, one for metadata and one for schema. Raises: @@ -750,7 +752,7 @@ def parquet_handler( load_parquet: bool = False, save_parquet: bool = False, force_recreate: bool = False, - ) -> Tuple[dd.DataFrame, dd.DataFrame]: + ) -> tuple[dd.DataFrame, dd.DataFrame]: """ Handles loading and saving of parquet files based on the provided parameters. @@ -765,7 +767,7 @@ def parquet_handler( save_parquet (bool, optional): Saves the entire dataframe into a parquet. force_recreate (bool, optional): Forces recreation of buffer file. Returns: - tuple: A tuple containing two dataframes: + tuple[dd.DataFrame, dd.DataFrame]: A tuple containing two dataframes: - dataframe_electron: Dataframe containing the loaded/augmented electron data. - dataframe_pulse: Dataframe containing the loaded/augmented timed data. @@ -807,7 +809,7 @@ def parquet_handler( dataframe = dd.read_parquet(filenames, calculate_divisions=True) # Channels to fill NaN values - channels: List[str] = self.get_channels(["per_pulse", "per_train"]) + channels: list[str] = self.get_channels(["per_pulse", "per_train"]) overlap = min(file.num_rows for file in metadata) @@ -864,23 +866,23 @@ def get_elapsed_time(self, fids=None, **kwds): # noqa: ARG002 def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, **kwds, - ) -> Tuple[dd.DataFrame, dd.DataFrame, dict]: + ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ Read express data from the DAQ, generating a parquet in between. Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files are stored + files (str | Sequence[str], optional): File path(s) to process. Defaults to None. + folders (str | Sequence[str], optional): Path to folder(s) where files are stored Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding files will + runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes precendence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". @@ -888,7 +890,8 @@ def read_dataframe( collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. Returns: - Tuple[dd.DataFrame, dict]: A tuple containing the concatenated DataFrame and metadata. + tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame + and metadata. Raises: ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. diff --git a/sed/loader/flash/metadata.py b/sed/loader/flash/metadata.py index 9f23b59a..50fd69b1 100644 --- a/sed/loader/flash/metadata.py +++ b/sed/loader/flash/metadata.py @@ -2,10 +2,9 @@ The module provides a MetadataRetriever class for retrieving metadata from a Scicat Instance based on beamtime and run IDs. """ +from __future__ import annotations import warnings -from typing import Dict -from typing import Optional import requests @@ -16,7 +15,7 @@ class MetadataRetriever: on beamtime and run IDs. """ - def __init__(self, metadata_config: Dict, scicat_token: str = None) -> None: + def __init__(self, metadata_config: dict, scicat_token: str = None) -> None: """ Initializes the MetadataRetriever class. @@ -43,15 +42,15 @@ def get_metadata( self, beamtime_id: str, runs: list, - metadata: Optional[Dict] = None, - ) -> Dict: + metadata: dict = None, + ) -> dict: """ Retrieves metadata for a given beamtime ID and list of runs. Args: beamtime_id (str): The ID of the beamtime. runs (list): A list of run IDs. - metadata (Dict, optional): The existing metadata dictionary. + metadata (dict, optional): The existing metadata dictionary. Defaults to None. Returns: @@ -75,7 +74,7 @@ def get_metadata( return metadata - def _get_metadata_per_run(self, pid: str) -> Dict: + def _get_metadata_per_run(self, pid: str) -> dict: """ Retrieves metadata for a specific run based on the PID. @@ -83,13 +82,13 @@ def _get_metadata_per_run(self, pid: str) -> Dict: pid (str): The PID of the run. Returns: - Dict: The retrieved metadata. + dict: The retrieved metadata. Raises: Exception: If the request to retrieve metadata fails. """ headers2 = dict(self.headers) - headers2["Authorization"] = "Bearer {}".format(self.token) + headers2["Authorization"] = f"Bearer {self.token}" try: dataset_response = requests.get( @@ -101,7 +100,9 @@ def _get_metadata_per_run(self, pid: str) -> Dict: # Check if response is an empty object because wrong url for older implementation if not dataset_response.content: dataset_response = requests.get( - self._create_old_dataset_url(pid), headers=headers2, timeout=10 + self._create_old_dataset_url(pid), + headers=headers2, + timeout=10, ) # If the dataset request is successful, return the retrieved metadata # as a JSON object @@ -113,12 +114,16 @@ def _get_metadata_per_run(self, pid: str) -> Dict: def _create_old_dataset_url(self, pid: str) -> str: return "{burl}/{url}/%2F{npid}".format( - burl=self.url, url="Datasets", npid=self._reformat_pid(pid) + burl=self.url, + url="Datasets", + npid=self._reformat_pid(pid), ) def _create_new_dataset_url(self, pid: str) -> str: return "{burl}/{url}/{npid}".format( - burl=self.url, url="Datasets", npid=self._reformat_pid(pid) + burl=self.url, + url="Datasets", + npid=self._reformat_pid(pid), ) def _reformat_pid(self, pid: str) -> str: diff --git a/sed/loader/generic/loader.py b/sed/loader/generic/loader.py index f3cd5f25..e5149968 100644 --- a/sed/loader/generic/loader.py +++ b/sed/loader/generic/loader.py @@ -3,10 +3,9 @@ Mostly ported from https://github.com/mpes-kit/mpes. @author: L. Rettig """ -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union +from __future__ import annotations + +from collections.abc import Sequence import dask.dataframe as ddf import numpy as np @@ -29,23 +28,23 @@ class GenericLoader(BaseLoader): def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = "parquet", metadata: dict = None, collect_metadata: bool = False, **kwds, - ) -> Tuple[ddf.DataFrame, ddf.DataFrame, dict]: + ) -> tuple[ddf.DataFrame, ddf.DataFrame, dict]: """Read stored files from a folder into a dataframe. Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. + files (str | Sequence[str], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files + folders (str | Sequence[str], optional): Path to folder(s) where files are stored. Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding + runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes precendence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File type to read ('parquet', 'json', 'csv', etc). @@ -64,7 +63,7 @@ def read_dataframe( ValueError: Raised if the file type is not supported. Returns: - Tuple[ddf.DataFrame, dict]: Dask dataframe, timed dataframe and metadata + tuple[ddf.DataFrame, ddf.DataFrame, dict]: Dask dataframe, timed dataframe and metadata read from specified files. """ # pylint: disable=duplicate-code @@ -102,21 +101,21 @@ def read_dataframe( def get_files_from_run_id( self, run_id: str, # noqa: ARG002 - folders: Union[str, Sequence[str]] = None, # noqa: ARG002 + folders: str | Sequence[str] = None, # noqa: ARG002 extension: str = None, # noqa: ARG002 **kwds, # noqa: ARG002 - ) -> List[str]: + ) -> list[str]: """Locate the files for a given run identifier. Args: run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to None. extension (str, optional): The file extension. Defaults to "h5". kwds: Keyword arguments Return: - str: Path to the location of run data. + list[str]: Path to the location of run data. """ raise NotImplementedError @@ -124,7 +123,7 @@ def get_count_rate( self, fids: Sequence[int] = None, # noqa: ARG002 **kwds, # noqa: ARG002 - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Create count rate data for the files specified in ``fids``. Args: @@ -133,7 +132,7 @@ def get_count_rate( kwds: Keyword arguments Return: - Tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds + tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds into the scan. """ # TODO diff --git a/sed/loader/loader_interface.py b/sed/loader/loader_interface.py index 1cedb094..6a636861 100644 --- a/sed/loader/loader_interface.py +++ b/sed/loader/loader_interface.py @@ -1,9 +1,10 @@ """Interface to select a specified loader """ +from __future__ import annotations + import glob import importlib.util import os -from typing import List from sed.loader.base.loader import BaseLoader @@ -43,11 +44,11 @@ def get_loader( return module.LOADER(config=config) -def get_names_of_all_loaders() -> List[str]: +def get_names_of_all_loaders() -> list[str]: """Helper function to populate a list of all available loaders. Returns: - List[str]: List of all detected loader names. + list[str]: List of all detected loader names. """ path_prefix = f"{os.path.dirname(__file__)}{os.sep}" if os.path.dirname(__file__) else "" files = glob.glob(os.path.join(path_prefix, "*", "loader.py")) diff --git a/sed/loader/mirrorutil.py b/sed/loader/mirrorutil.py index 1946a171..e443b329 100644 --- a/sed/loader/mirrorutil.py +++ b/sed/loader/mirrorutil.py @@ -5,11 +5,12 @@ Mostly ported from https://github.com/mpes-kit/mpes. @author: L. Rettig """ +from __future__ import annotations + import errno import os import shutil from datetime import datetime -from typing import List import dask as d from dask.diagnostics import ProgressBar @@ -317,7 +318,7 @@ def get_target_dir( # replacement for os.makedirs, which is independent of umask -def mymakedirs(path: str, mode: int, gid: int) -> List[str]: +def mymakedirs(path: str, mode: int, gid: int) -> list[str]: """Creates a directory path iteratively from its root Args: @@ -326,7 +327,7 @@ def mymakedirs(path: str, mode: int, gid: int) -> List[str]: gid (int): Group id of created directories Returns: - str: Path of created directories + list[str]: Path of created directories """ if not path or os.path.exists(path): diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 7674251b..c28b32f3 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -3,15 +3,13 @@ Mostly ported from https://github.com/mpes-kit/mpes. @author: L. Rettig """ +from __future__ import annotations + import datetime import glob import json import os -from typing import Dict -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union +from collections.abc import Sequence from urllib.error import HTTPError from urllib.error import URLError from urllib.request import urlopen @@ -30,7 +28,7 @@ def hdf5_to_dataframe( files: Sequence[str], group_names: Sequence[str] = None, - alias_dict: Dict[str, str] = None, + alias_dict: dict[str, str] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", ms_markers_group: str = "msMarkers", @@ -44,7 +42,7 @@ def hdf5_to_dataframe( files (List[str]): A list of the file paths to load. group_names (List[str], optional): hdf5 group names to load. Defaults to load all groups containing "Stream" - alias_dict (Dict[str, str], optional): Dictionary of aliases for the dataframe + alias_dict (dict[str, str], optional): Dictionary of aliases for the dataframe columns. Keys are the hdf5 groupnames, and values the aliases. If an alias is not found, its group name is used. Defaults to read the attribute "Name" from each group. @@ -110,7 +108,7 @@ def hdf5_to_dataframe( def hdf5_to_timed_dataframe( files: Sequence[str], group_names: Sequence[str] = None, - alias_dict: Dict[str, str] = None, + alias_dict: dict[str, str] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", ms_markers_group: str = "msMarkers", @@ -125,7 +123,7 @@ def hdf5_to_timed_dataframe( files (List[str]): A list of the file paths to load. group_names (List[str], optional): hdf5 group names to load. Defaults to load all groups containing "Stream" - alias_dict (Dict[str, str], optional): Dictionary of aliases for the dataframe + alias_dict (dict[str, str], optional): Dictionary of aliases for the dataframe columns. Keys are the hdf5 groupnames, and values the aliases. If an alias is not found, its group name is used. Defaults to read the attribute "Name" from each group. @@ -192,7 +190,7 @@ def get_groups_and_aliases( h5file: h5py.File, seach_pattern: str = None, alias_key: str = "Name", -) -> Tuple[List[str], Dict[str, str]]: +) -> tuple[list[str], dict[str, str]]: """Read groups and aliases from a provided hdf5 file handle Args: @@ -204,7 +202,7 @@ def get_groups_and_aliases( Attribute key where aliases are stored. Defaults to "Name". Returns: - Tuple[List[str], Dict[str, str]]: + tuple[list[str], dict[str, str]]: The list of groupnames and the alias dictionary parsed from the file """ # get group names: @@ -401,7 +399,7 @@ def get_attribute(h5group: h5py.Group, attribute: str) -> str: def get_count_rate( h5file: h5py.File, ms_markers_group: str = "msMarkers", -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Create count rate in the file from the msMarker column. Args: @@ -410,7 +408,7 @@ def get_count_rate( are stored. Defaults to "msMarkers". Returns: - Tuple[np.ndarray, np.ndarray]: The count rate in Hz and the seconds into the + tuple[np.ndarray, np.ndarray]: The count rate in Hz and the seconds into the scan. """ ms_markers = np.asarray(h5file[ms_markers_group]) @@ -446,7 +444,7 @@ def get_archiver_data( archiver_channel: str, ts_from: float, ts_to: float, -) -> Tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray]: """Extract time stamps and corresponding data from and EPICS archiver instance Args: @@ -456,7 +454,7 @@ def get_archiver_data( ts_to (float): ending time stamp of the range of interest Returns: - Tuple[List, List]: The extracted time stamps and corresponding data + tuple[np.ndarray, np.ndarray]: The extracted time stamps and corresponding data """ iso_from = datetime.datetime.utcfromtimestamp(ts_from).isoformat() iso_to = datetime.datetime.utcfromtimestamp(ts_to).isoformat() @@ -495,25 +493,25 @@ def __init__( def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, time_stamps: bool = False, **kwds, - ) -> Tuple[ddf.DataFrame, ddf.DataFrame, dict]: + ) -> tuple[ddf.DataFrame, ddf.DataFrame, dict]: """Read stored hdf5 files from a list or from folder and returns a dask dataframe and corresponding metadata. Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. + files (str | Sequence[str], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files + folders (str | Sequence[str], optional): Path to folder(s) where files are stored. Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding + runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes precendence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File extension to use. If a folder path is given, @@ -541,7 +539,7 @@ def read_dataframe( FileNotFoundError: Raised if a file or folder is not found. Returns: - Tuple[ddf.DataFrame, ddf.DataFrame, dict]: Dask dataframe, timed Dask + tuple[ddf.DataFrame, ddf.DataFrame, dict]: Dask dataframe, timed Dask dataframe and metadata read from specified files. """ # if runs is provided, try to locate the respective files relative to the provided folder. @@ -632,21 +630,21 @@ def read_dataframe( def get_files_from_run_id( self, run_id: str, - folders: Union[str, Sequence[str]] = None, + folders: str | Sequence[str] = None, extension: str = "h5", **kwds, # noqa: ARG002 - ) -> List[str]: + ) -> list[str]: """Locate the files for a given run identifier. Args: run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"] extension (str, optional): The file extension. Defaults to "h5". kwds: Keyword arguments Return: - List[str]: List of file path strings to the location of run data. + list[str]: List of file path strings to the location of run data. """ if folders is None: folders = self._config["core"]["paths"]["data_raw_dir"] @@ -654,7 +652,7 @@ def get_files_from_run_id( if isinstance(folders, str): folders = [folders] - files: List[str] = [] + files: list[str] = [] for folder in folders: run_files = natsorted( glob.glob( @@ -673,11 +671,11 @@ def get_files_from_run_id( # Return the list of found files return files - def get_start_and_end_time(self) -> Tuple[float, float]: + def get_start_and_end_time(self) -> tuple[float, float]: """Extract the start and end time stamps from the loaded files Returns: - Tuple[float, float]: A tuple containing the start and end time stamps + tuple[float, float]: A tuple containing the start and end time stamps """ h5file = h5py.File(self.files[0]) timestamps = hdf5_to_array( @@ -884,7 +882,7 @@ def get_count_rate( self, fids: Sequence[int] = None, **kwds, - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Create count rate from the msMarker column for the files specified in ``fids``. @@ -896,7 +894,7 @@ def get_count_rate( - **ms_markers_group**: Name of the hdf5 group containing the ms-markers Returns: - Tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds + tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds into the scan. """ if fids is None: diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 67faca4c..b8a7ad29 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -9,13 +9,12 @@ sed funtionality. Most of the structure is identical to the FLASH loader. """ +from __future__ import annotations + import time +from collections.abc import Sequence from functools import reduce from pathlib import Path -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as dd import h5py @@ -50,15 +49,15 @@ def __init__(self, config: dict) -> None: self.multi_index = ["trainId", "pulseId", "electronId"] self.index_per_electron: MultiIndex = None self.index_per_pulse: MultiIndex = None - self.failed_files_error: List[str] = [] - self.array_indices: List[List[slice]] = None + self.failed_files_error: list[str] = [] + self.array_indices: list[list[slice]] = None - def initialize_paths(self) -> Tuple[List[Path], Path]: + def initialize_paths(self) -> tuple[list[Path], Path]: """ Initializes the paths based on the configuration. Returns: - Tuple[List[Path], Path]: A tuple containing a list of raw data directories + tuple[List[Path], Path]: A tuple containing a list of raw data directories paths and the parquet data directory path. Raises: @@ -107,23 +106,23 @@ def initialize_paths(self) -> Tuple[List[Path], Path]: def get_files_from_run_id( self, run_id: str, - folders: Union[str, Sequence[str]] = None, + folders: str | Sequence[str] = None, extension: str = "h5", **kwds, - ) -> List[str]: + ) -> list[str]: """Returns a list of filenames for a given run located in the specified directory for the specified data acquisition (daq). Args: run_id (str): The run identifier to locate. - folders (Union[str, Sequence[str]], optional): The directory(ies) where the raw + folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"]. extension (str, optional): The file extension. Defaults to "h5". kwds: Keyword arguments: - daq (str): The data acquisition identifier. Returns: - List[str]: A list of path strings representing the collected file names. + list[str]: A list of path strings representing the collected file names. Raises: FileNotFoundError: If no files are found for the given run in the directory. @@ -147,7 +146,7 @@ def get_files_from_run_id( # Generate the file patterns to search for in the directory file_pattern = f"**/{stream_name_prefixes[daq]}{run_id}{stream_name_postfix}*." + extension - files: List[Path] = [] + files: list[Path] = [] # Use pathlib to search for matching files in each directory for folder in folders: files.extend( @@ -167,7 +166,7 @@ def get_files_from_run_id( return [str(file.resolve()) for file in files] @property - def available_channels(self) -> List: + def available_channels(self) -> list: """Returns the channel names that are available for use, excluding pulseId, defined by the json file""" available_channels = list(self._config["dataframe"]["channels"].keys()) @@ -175,13 +174,13 @@ def available_channels(self) -> List: available_channels.remove("trainId") return available_channels - def get_channels(self, formats: Union[str, List[str]] = "", index: bool = False) -> List[str]: + def get_channels(self, formats: str | list[str] = "", index: bool = False) -> list[str]: """ Returns a list of channels associated with the specified format(s). Args: - formats (Union[str, List[str]]): The desired format(s) - ('per_pulse', 'per_electron', 'per_train', 'all'). + formats (str | list[str]): The desired format(s) + ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multi_index. Returns: @@ -342,7 +341,7 @@ def create_numpy_array_per_channel( self, h5_file: h5py.File, channel: str, - ) -> Tuple[Series, np.ndarray]: + ) -> tuple[Series, np.ndarray]: """ Returns a numpy array for a given channel name for a given file. @@ -351,7 +350,7 @@ def create_numpy_array_per_channel( channel (str): The name of the channel. Returns: - Tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array + tuple[Series, np.ndarray]: A tuple containing the train ID Series and the numpy array for the channel's data. """ @@ -511,7 +510,7 @@ def create_dataframe_per_channel( self, h5_file: h5py.File, channel: str, - ) -> Union[Series, DataFrame]: + ) -> Series | DataFrame: """ Returns a pandas DataFrame for a given channel name from a given file. @@ -524,7 +523,7 @@ def create_dataframe_per_channel( channel (str): The name of the channel. Returns: - Union[Series, DataFrame]: A pandas Series or DataFrame representing the channel's data. + Series | DataFrame: A pandas Series or DataFrame representing the channel's data. Raises: ValueError: If the channel has an undefined format. @@ -658,7 +657,7 @@ def create_dataframe_per_file( df = split_dld_time_from_sector_id(df, config=self._config) return df - def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, Exception]: + def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> bool | Exception: """ Converts an HDF5 file to Parquet format to create a buffer file. @@ -669,6 +668,9 @@ def create_buffer_file(self, h5_path: Path, parquet_path: Path) -> Union[bool, E h5_path (Path): Path to the input HDF5 file. parquet_path (Path): Path to the output Parquet file. + Returns: + bool | Exception: Collected exceptions if any. + Raises: ValueError: If an error occurs during the conversion process. @@ -689,7 +691,7 @@ def buffer_file_handler( data_parquet_dir: Path, detector: str, force_recreate: bool, - ) -> Tuple[List[Path], List, List]: + ) -> tuple[list[Path], list, list]: """ Handles the conversion of buffer files (h5 to parquet) and returns the filenames. @@ -699,7 +701,7 @@ def buffer_file_handler( force_recreate (bool): Forces recreation of buffer files Returns: - Tuple[List[Path], List, List]: Three lists, one for + tuple[list[Path], list, list]: Three lists, one for parquet file paths, one for metadata and one for schema. Raises: @@ -796,7 +798,7 @@ def parquet_handler( load_parquet: bool = False, save_parquet: bool = False, force_recreate: bool = False, - ) -> Tuple[dd.DataFrame, dd.DataFrame]: + ) -> tuple[dd.DataFrame, dd.DataFrame]: """ Handles loading and saving of parquet files based on the provided parameters. @@ -811,7 +813,7 @@ def parquet_handler( save_parquet (bool, optional): Saves the entire dataframe into a parquet. force_recreate (bool, optional): Forces recreation of buffer file. Returns: - tuple: A tuple containing two dataframes: + tuple[dd.DataFrame, dd.DataFrame]: A tuple containing two dataframes: - dataframe_electron: Dataframe containing the loaded/augmented electron data. - dataframe_pulse: Dataframe containing the loaded/augmented timed data. @@ -852,7 +854,7 @@ def parquet_handler( dataframe = dd.read_parquet(filenames, calculate_divisions=True) # Channels to fill NaN values - channels: List[str] = self.get_channels(["per_pulse", "per_train"]) + channels: list[str] = self.get_channels(["per_pulse", "per_train"]) overlap = min(file.num_rows for file in metadata) @@ -908,23 +910,23 @@ def get_elapsed_time(self, fids=None, **kwds): # noqa: ARG002 def read_dataframe( self, - files: Union[str, Sequence[str]] = None, - folders: Union[str, Sequence[str]] = None, - runs: Union[str, Sequence[str]] = None, + files: str | Sequence[str] = None, + folders: str | Sequence[str] = None, + runs: str | Sequence[str] = None, ftype: str = "h5", metadata: dict = None, collect_metadata: bool = False, **kwds, - ) -> Tuple[dd.DataFrame, dd.DataFrame, dict]: + ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ Read express data from the DAQ, generating a parquet in between. Args: - files (Union[str, Sequence[str]], optional): File path(s) to process. Defaults to None. - folders (Union[str, Sequence[str]], optional): Path to folder(s) where files are stored + files (str | Sequence[str], optional): File path(s) to process. Defaults to None. + folders (str | Sequence[str], optional): Path to folder(s) where files are stored Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (Union[str, Sequence[str]], optional): Run identifier(s). Corresponding files will + runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes precendence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". @@ -932,7 +934,8 @@ def read_dataframe( collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. Returns: - Tuple[dd.DataFrame, dict]: A tuple containing the concatenated DataFrame and metadata. + tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame + and metadata. Raises: ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. diff --git a/sed/loader/utils.py b/sed/loader/utils.py index ab3fde3a..ba3778df 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -1,10 +1,10 @@ """Utilities for loaders """ +from __future__ import annotations + +from collections.abc import Sequence from glob import glob from typing import cast -from typing import List -from typing import Sequence -from typing import Union import dask.dataframe import numpy as np @@ -21,7 +21,7 @@ def gather_files( f_end: int = None, f_step: int = 1, file_sorting: bool = True, -) -> List[str]: +) -> list[str]: """Collects and sorts files with specified extension from a given folder. Args: @@ -37,13 +37,13 @@ def gather_files( Defaults to True. Returns: - List[str]: List of collected file names. + list[str]: List of collected file names. """ try: files = glob(folder + "/*." + extension) if file_sorting: - files = cast(List[str], natsorted(files)) + files = cast(list[str], natsorted(files)) if f_start is not None and f_end is not None: files = files[slice(f_start, f_end, f_step)] @@ -55,7 +55,7 @@ def gather_files( return files -def parse_h5_keys(h5_file: File, prefix: str = "") -> List[str]: +def parse_h5_keys(h5_file: File, prefix: str = "") -> list[str]: """Helper method which parses the channels present in the h5 file Args: h5_file (h5py.File): The H5 file object. @@ -63,7 +63,7 @@ def parse_h5_keys(h5_file: File, prefix: str = "") -> List[str]: Defaults to an empty string. Returns: - List[str]: A list of channel names in the H5 file. + list[str]: A list of channel names in the H5 file. Raises: Exception: If an error occurs while parsing the keys. @@ -144,19 +144,19 @@ def split_channel_bitwise( def split_dld_time_from_sector_id( - df: Union[pd.DataFrame, dask.dataframe.DataFrame], + df: pd.DataFrame | dask.dataframe.DataFrame, tof_column: str = None, sector_id_column: str = None, sector_id_reserved_bits: int = None, config: dict = None, -) -> Union[pd.DataFrame, dask.dataframe.DataFrame]: +) -> pd.DataFrame | dask.dataframe.DataFrame: """Converts the 8s time in steps to time in steps and sectorID. The 8s detector encodes the dldSectorID in the 3 least significant bits of the dldTimeSteps channel. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. tof_column (str, optional): Name of the column containing the time-of-flight steps. Defaults to config["dataframe"]["tof_column"]. sector_id_column (str, optional): Name of the column containing the @@ -165,7 +165,7 @@ def split_dld_time_from_sector_id( config (dict, optional): Configuration dictionary. Defaults to None. Returns: - Union[pd.DataFrame, dask.dataframe.DataFrame]: Dataframe with the new columns. + pd.DataFrame | dask.dataframe.DataFrame: Dataframe with the new columns. """ if tof_column is None: if config is None: From 1284b362c0c911f01f668bca1fc9269b11912ee1 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 23:28:22 +0200 Subject: [PATCH 084/300] more type fixes --- tests/calibrator/test_delay.py | 5 ++- tests/calibrator/test_energy.py | 17 ++++---- tests/calibrator/test_momentum.py | 7 ++-- tests/helpers.py | 2 + tests/loader/flash/test_flash_loader.py | 2 + tests/loader/flash/test_flash_metadata.py | 3 ++ tests/loader/sxp/test_sxp_loader.py | 6 +-- tests/loader/test_loaders.py | 5 ++- tests/loader/test_mirrorutil.py | 2 + tests/loader/test_utils.py | 2 + tests/test_binning.py | 49 +++++++++++------------ tests/test_config.py | 2 + tests/test_dataset.py | 4 ++ tests/test_dfops.py | 5 ++- tests/test_diagnostics.py | 2 + tests/test_io.py | 2 + tests/test_metadata.py | 7 +++- tests/test_processor.py | 9 ++--- 18 files changed, 79 insertions(+), 52 deletions(-) diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index 6a848e83..d7677296 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -1,9 +1,10 @@ """Module tests.calibrator.delay, tests for the sed.calibrator.delay file """ +from __future__ import annotations + import os from importlib.util import find_spec from typing import Any -from typing import Dict import dask.dataframe import numpy as np @@ -114,7 +115,7 @@ def test_delay_parameters_from_delay_range_mm() -> None: collect_metadata=False, ) dc = DelayCalibrator(config=config) - calibration: Dict[str, Any] = {"delay_range_mm": (1, 15)} + calibration: dict[str, Any] = {"delay_range_mm": (1, 15)} with pytest.raises(NotImplementedError): dc.append_delay_axis(df, calibration=calibration) calibration["time0"] = 1 diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 0c1735fb..0ea4a062 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -1,5 +1,7 @@ """Module tests.calibrator.energy, tests for the sed.calibrator.energy module """ +from __future__ import annotations + import csv import glob import itertools @@ -7,7 +9,6 @@ from copy import deepcopy from importlib.util import find_spec from typing import Any -from typing import Dict from typing import Literal import dask.dataframe @@ -391,7 +392,7 @@ def test_energy_correction(correction_type: str, correction_kwd: dict) -> None: config=config, loader=get_loader("mpes", config=config), ) - correction: Dict[Any, Any] = { + correction: dict[Any, Any] = { "correction_type": correction_type, "amplitude": amplitude, "center": center, @@ -437,7 +438,7 @@ def test_adjust_energy_correction_raises(correction_type: str) -> None: config=config, loader=get_loader("mpes", config=config), ) - correction_dict: Dict[str, Any] = { + correction_dict: dict[str, Any] = { "correction_type": correction_type, "amplitude": amplitude, "center": center, @@ -482,7 +483,7 @@ def test_energy_correction_from_dict_kwds(correction_type: str, correction_kwd: config=config, loader=get_loader("mpes", config=config), ) - correction_dict: Dict[str, Any] = { + correction_dict: dict[str, Any] = { "correction_type": correction_type, "amplitude": amplitude, "center": center, @@ -542,7 +543,7 @@ def test_apply_energy_correction_raises(correction_type: str) -> None: config=config, loader=get_loader("mpes", config=config), ) - correction_dict: Dict[str, Any] = { + correction_dict: dict[str, Any] = { "correction_type": correction_type, "amplitude": amplitude, "center": center, @@ -621,7 +622,7 @@ def test_add_offsets_functionality(energy_scale: str) -> None: exp_vals -= df["off2"] * scale_sign exp_vals += df["off3"].mean() * scale_sign np.testing.assert_allclose(res["energy"].values, exp_vals.values) - exp_meta: Dict[str, Any] = {} + exp_meta: dict[str, Any] = {} exp_meta["applied"] = True exp_meta["offsets"] = ec.offsets assert meta == exp_meta @@ -655,7 +656,7 @@ def test_add_offsets_functionality(energy_scale: str) -> None: def test_add_offset_raises() -> None: """test if add_offset raises the correct errors""" - cfg_dict: Dict[str, Any] = { + cfg_dict: dict[str, Any] = { "energy": { "calibration": { "energy_scale": "kinetic", @@ -714,7 +715,7 @@ def test_add_offset_raises() -> None: def test_align_dld_sectors() -> None: """test functionality and error handling of align_dld_sectors""" - cfg_dict: Dict[str, Any] = { + cfg_dict: dict[str, Any] = { "dataframe": { "tof_column": "dldTimeSteps", "sector_id_column": "dldSectorId", diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index c65bfc28..88f050de 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -1,11 +1,12 @@ """Module tests.calibrator.momentum, tests for the sed.calibrator.momentum module """ +from __future__ import annotations + import csv import glob import os from importlib.util import find_spec from typing import Any -from typing import Dict import numpy as np import pytest @@ -280,8 +281,8 @@ def test_apply_correction() -> None: zip(transformations_list, depends_on_list), ) def test_apply_registration( - transformations: Dict[Any, Any], - depends_on: Dict[Any, Any], + transformations: dict[Any, Any], + depends_on: dict[Any, Any], ) -> None: """Test the application of the distortion correction to the dataframe.""" config = parse_config( diff --git a/tests/helpers.py b/tests/helpers.py index c7f6d3e3..2264bee3 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,7 @@ """This file contains helper functions used in other tests. """ +from __future__ import annotations + import numpy as np import xarray as xr diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index edff997e..88276107 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -1,4 +1,6 @@ """Tests for FlashLoader functionality""" +from __future__ import annotations + import os from importlib.util import find_spec from pathlib import Path diff --git a/tests/loader/flash/test_flash_metadata.py b/tests/loader/flash/test_flash_metadata.py index ef5305ad..af376d0d 100644 --- a/tests/loader/flash/test_flash_metadata.py +++ b/tests/loader/flash/test_flash_metadata.py @@ -1,3 +1,6 @@ +"""Tests for FlashLoader metadata functionality""" +from __future__ import annotations + import pytest from sed.loader.flash.metadata import MetadataRetriever diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 3332f231..83ade005 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -1,9 +1,9 @@ -# pylint: disable=duplicate-code """Tests for SXPLoader functionality""" +from __future__ import annotations + import os from importlib.util import find_spec from pathlib import Path -from typing import List import pytest @@ -35,7 +35,7 @@ def test_get_channels_by_format(config_file: dict) -> None: # Define expected channels for each format. electron_channels = ["dldPosX", "dldPosY", "dldTimeSteps"] - pulse_channels: List[str] = [] + pulse_channels: list[str] = [] train_channels = ["timeStamp", "delayStage"] index_channels = ["trainId", "pulseId", "electronId"] diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index f638ba0d..8958b1a1 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -1,11 +1,12 @@ """Test cases for loaders used to load dataframes """ +from __future__ import annotations + import os from copy import deepcopy from importlib.util import find_spec from pathlib import Path from typing import cast -from typing import List import dask.dataframe as ddf import pytest @@ -52,7 +53,7 @@ def get_loader_name_from_loader_object(loader: BaseLoader) -> str: return "" -def get_all_loaders() -> List[ParameterSet]: +def get_all_loaders() -> list[ParameterSet]: """Scans through the loader list and returns them for pytest parametrization""" loaders = [] diff --git a/tests/loader/test_mirrorutil.py b/tests/loader/test_mirrorutil.py index db402108..468c20fb 100644 --- a/tests/loader/test_mirrorutil.py +++ b/tests/loader/test_mirrorutil.py @@ -1,5 +1,7 @@ """Module tests.loader.mirrorutil, tests for the sed.load.mirrorutil file """ +from __future__ import annotations + import glob import io import os diff --git a/tests/loader/test_utils.py b/tests/loader/test_utils.py index 04a10748..6f342c7f 100644 --- a/tests/loader/test_utils.py +++ b/tests/loader/test_utils.py @@ -1,5 +1,7 @@ """Module tests.loader.test_utils, tests for the sed.load.utils file """ +from __future__ import annotations + import dask.dataframe as dd import numpy as np import pandas as pd diff --git a/tests/test_binning.py b/tests/test_binning.py index 69a5d1b9..dedbec10 100644 --- a/tests/test_binning.py +++ b/tests/test_binning.py @@ -1,11 +1,10 @@ """This file contains code that performs several tests for the sed.binning module """ +from __future__ import annotations + +from collections.abc import Sequence from typing import Any from typing import cast -from typing import List -from typing import Sequence -from typing import Tuple -from typing import Union import dask.dataframe as ddf import numpy as np @@ -64,12 +63,12 @@ [bins[:1], bins[:2], bins[:3]], ids=lambda x: f"bins:{len(x)}", ) -def test_histdd_error_is_raised(_samples: np.ndarray, _bins: List[int]) -> None: +def test_histdd_error_is_raised(_samples: np.ndarray, _bins: list[int]) -> None: """Test if the correct error is raised if the bins and sample shapes do not match Args: _samples (np.ndarray): Samples array - _bins (List[Tuple]): Bins list + _bins (list[int]): Bins list """ with pytest.raises(ValueError): if _samples.shape[1] == len(_bins): @@ -95,12 +94,12 @@ def test_histdd_error_is_raised(_samples: np.ndarray, _bins: List[int]) -> None: if x[2] < 7 else f"ndim: {x[2]-6}-round", ) -def test_histdd_bins_as_numpy(args: Tuple[np.ndarray, np.ndarray, int]) -> None: +def test_histdd_bins_as_numpy(args: tuple[np.ndarray, np.ndarray, int]) -> None: """Test whether the numba_histogramdd functions produces the same result as np.histogramdd if called with a list of bin edgees Args: - args (Tuple[np.ndarray, np.ndarray, int]): Tuple of + args (tuple[np.ndarray, np.ndarray, int]): Tuple of (samples, bin_edges, dimension) """ sample_, bins_, _ = args @@ -128,12 +127,12 @@ def test_histdd_bins_as_numpy(args: Tuple[np.ndarray, np.ndarray, int]) -> None: if x[3] < 7 else f"ndim: {x[3]-6}-round", ) -def test_histdd_ranges_as_numpy(args: Tuple[np.ndarray, tuple, tuple, int]) -> None: +def test_histdd_ranges_as_numpy(args: tuple[np.ndarray, tuple, tuple, int]) -> None: """Test whether the numba_histogramdd functions produces the same result as np.histogramdd if called with bin numbers and ranges Args: - args (Tuple[np.ndarray, np.ndarray, np.ndarray, int]): Tuple of + args (tuple[np.ndarray, np.ndarray, np.ndarray, int]): Tuple of (samples, bins, ranges, dimension) """ sample_, bins_, ranges_, _ = args @@ -161,12 +160,12 @@ def test_histdd_ranges_as_numpy(args: Tuple[np.ndarray, tuple, tuple, int]) -> N if x[3] < 7 else f"ndim: {x[3]-6}-round", ) -def test_histdd_one_bins_as_numpy(args: Tuple[np.ndarray, int, tuple, int]) -> None: +def test_histdd_one_bins_as_numpy(args: tuple[np.ndarray, int, tuple, int]) -> None: """Test whether the numba_histogramdd functions produces the same result as np.histogramdd if called with bin numbers and ranges Args: - args (Tuple[np.ndarray, np.ndarray, np.ndarray, int]): Tuple of + args (tuple[np.ndarray, np.ndarray, np.ndarray, int]): Tuple of (samples, bins, ranges, dimension) """ sample_, bins_, ranges_, _ = args @@ -195,13 +194,13 @@ def test_histdd_one_bins_as_numpy(args: Tuple[np.ndarray, int, tuple, int]) -> N else f"ndim: {x[4]-6}-round", ) def test_from_bins_equals_from_bin_range( - args: Tuple[np.ndarray, int, tuple, np.ndarray, int], + args: tuple[np.ndarray, int, tuple, np.ndarray, int], ) -> None: """Test whether the numba_histogramdd functions produces the same result if called with bin numbers and ranges or with bin edges. Args: - args (Tuple[np.ndarray, int, tuple, np.ndarray, int]): Tuple of + args (tuple[np.ndarray, int, tuple, np.ndarray, int]): Tuple of (samples, bins, ranges, bin_edges, dimension) """ sample_, bins_, ranges_, arrays_, _ = args @@ -219,11 +218,11 @@ def test_from_bins_equals_from_bin_range( ], ids=lambda x: f"ndim: {x[2]}", ) -def test_numba_hist_from_bins(args: Tuple[np.ndarray, np.ndarray, int]) -> None: +def test_numba_hist_from_bins(args: tuple[np.ndarray, np.ndarray, int]) -> None: """Run tests using the _hist_from_bins function without numba jit. Args: - args (Tuple[np.ndarray, np.ndarray, int]): Tuple of + args (tuple[np.ndarray, np.ndarray, int]): Tuple of (samples, bin_edges, dimension) """ sample_, arrays_, _ = args @@ -253,11 +252,11 @@ def test_numba_hist_from_bins(args: Tuple[np.ndarray, np.ndarray, int]) -> None: ], ids=lambda x: f"ndim: {x[3]}", ) -def test_numba_hist_from_bins_ranges(args: Tuple[np.ndarray, int, tuple, int]) -> None: +def test_numba_hist_from_bins_ranges(args: tuple[np.ndarray, int, tuple, int]) -> None: """Run tests using the _hist_from_bins_ranges function without numba jit. Args: - args (Tuple[np.ndarray, int, tuple, int]): Tuple of + args (tuple[np.ndarray, int, tuple, int]): Tuple of (samples, bins, ranges, dimension) """ sample_, bins_, ranges_, _ = args @@ -316,18 +315,18 @@ def test_bin_edges_to_bin_centers() -> None: ], ) def test_simplify_binning_arguments( - args: Tuple[List[int], List[str], List[Tuple[float, float]]], + args: tuple[list[int], list[str], list[tuple[float, float]]], arg_type: str, ) -> None: """Test the result of the _simplify_binning_arguments functions for number of bins and ranges """ - bins_: Union[int, list, dict] = None - axes_: List[str] = None - ranges_: List[Tuple[float, float]] = None - bins_expected: List[Any] = None - axes_expected: List[Any] = None - ranges_expected: List[Any] = None + bins_: int | list | dict = None + axes_: list[str] = None + ranges_: list[tuple[float, float]] = None + bins_expected: list[Any] = None + axes_expected: list[Any] = None + ranges_expected: list[Any] = None bin_centers = [] for i in range(len(args[1])): diff --git a/tests/test_config.py b/tests/test_config.py index 79e41ac4..df875b45 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,7 @@ """This is a code that performs several tests for the settings loader. """ +from __future__ import annotations + import copy import os import tempfile diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 5082533e..4ff70ede 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,3 +1,7 @@ +"""This code performs several tests for the dataset module. +""" +from __future__ import annotations + import io import json import os diff --git a/tests/test_dfops.py b/tests/test_dfops.py index 2ce894dc..3a6482cd 100644 --- a/tests/test_dfops.py +++ b/tests/test_dfops.py @@ -1,8 +1,9 @@ """This file contains code that performs several tests for the dfops functions """ +from __future__ import annotations + import datetime as dt from typing import Any -from typing import List import dask.dataframe as ddf import numpy as np @@ -343,7 +344,7 @@ def test_offset_by_other_columns_functionality() -> None: offset_columns=["off1"], weights=[1], ) - expected: List[Any] = [11, 22, 33, 44, 55, 66] + expected: list[Any] = [11, 22, 33, 44, 55, 66] np.testing.assert_allclose(res["target"].values, expected) res = offset_by_other_columns( diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index ade40db9..d53ed8d9 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -1,5 +1,7 @@ """Module tests.diagnostics, tests for the sed.diagnostics module """ +from __future__ import annotations + import glob import itertools import os diff --git a/tests/test_io.py b/tests/test_io.py index 0f5f7abf..5725ab47 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,5 +1,7 @@ """This file contains code that performs several tests for the input/output functions """ +from __future__ import annotations + import os import random from pathlib import Path diff --git a/tests/test_metadata.py b/tests/test_metadata.py index fbe979a4..80672d0d 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,6 +1,9 @@ +"""This code performs several tests for the metadata handler module. +""" +from __future__ import annotations + import json from typing import Any -from typing import Dict import numpy as np import pytest @@ -8,7 +11,7 @@ from sed.core.metadata import DuplicateEntryError from sed.core.metadata import MetaHandler -metadata: Dict[Any, Any] = {} +metadata: dict[Any, Any] = {} metadata["entry_title"] = "Title" # sample metadata["sample"] = {} diff --git a/tests/test_processor.py b/tests/test_processor.py index 5471410a..cffb3a5b 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -1,5 +1,7 @@ """Module tests.processor, tests for the sed.core.processor module """ +from __future__ import annotations + import csv import glob import itertools @@ -8,9 +10,6 @@ from importlib.util import find_spec from pathlib import Path from typing import Any -from typing import Dict -from typing import List -from typing import Tuple import dask.dataframe as ddf import numpy as np @@ -556,7 +555,7 @@ def test_energy_calibration_workflow(energy_scale: str, calibration_method: str) ref_id = 5 rng = (66100, 67000) processor.find_bias_peaks(ranges=rng, ref_id=ref_id, infer_others=True, apply=True) - ranges: List[Tuple[Any, ...]] = [ + ranges: list[tuple[Any, ...]] = [ (64638.0, 65386.0), (64913.0, 65683.0), (65188.0, 65991.0), @@ -999,7 +998,7 @@ def test_get_normalization_histogram() -> None: # np.testing.assert_allclose(histogram1, histogram2) -metadata: Dict[Any, Any] = {} +metadata: dict[Any, Any] = {} metadata["entry_title"] = "Title" # User metadata["user0"] = {} From 3e02f48a87a574237babd08098bf48ede30d89b9 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 21 Jun 2024 23:36:49 +0200 Subject: [PATCH 085/300] forgotten changes --- sed/calibrator/energy.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 10e1a64c..07ec39bc 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -186,12 +186,12 @@ def bin_data( """Bin data from single-event files, and load into class. Args: - data_files (List[str]): list of file names to bin - axes (List[str], optional): bin axes. Defaults to + data_files (list[str]): list of file names to bin + axes (list[str], optional): bin axes. Defaults to config["dataframe"]["tof_column"]. - bins (List[int], optional): number of bins. + bins (list[int], optional): number of bins. Defaults to config["energy"]["bins"]. - ranges (Sequence[Tuple[float, float]], optional): bin ranges. + ranges (Sequence[tuple[float, float]], optional): bin ranges. Defaults to config["energy"]["ranges"]. biases (np.ndarray, optional): Bias voltages used. If not provided, biases are extracted from the file meta data. @@ -298,7 +298,7 @@ def adjust_ranges( (containing the peaks) among all traces. Args: - ranges (Tuple): + ranges (tuple): Collection of feature detection ranges, within which an algorithm (i.e. 1D peak detector) with look for the feature. ref_id (int, optional): Index of the reference trace. Defaults to 0. @@ -487,7 +487,7 @@ def feature_extract( """Select or extract the equivalent landmarks (e.g. peaks) among all traces. Args: - ranges (List[Tuple], optional): List of ranges in each trace to look for + ranges (list[tuple], optional): List of ranges in each trace to look for the peak feature, [start, end]. Defaults to self.featranges. traces (np.ndarray, optional): Collection of 1D spectra to use for calibration. Defaults to self.traces_normed. @@ -621,7 +621,7 @@ def view( # pylint: disable=dangerous-default-value Args: traces (np.ndarray): Matrix of traces to visualize. - segs (List[Tuple], optional): Segments to be highlighted in the + segs (list[tuple], optional): Segments to be highlighted in the visualization. Defaults to None. peaks (np.ndarray, optional): Peak positions for labelling the traces. Defaults to None. @@ -982,7 +982,7 @@ def adjust_energy_correction( Defaults to config["energy"]["correction_type"]. amplitude (float, optional): Amplitude of the time-of-flight correction term. Defaults to config["energy"]["correction"]["correction_type"]. - center (Tuple[float, float], optional): Center (x/y) coordinates for the + center (tuple[float, float], optional): Center (x/y) coordinates for the correction. Defaults to config["energy"]["correction"]["center"]. correction (dict, optional): Correction dict. Defaults to the config values and is updated from provided and adjusted parameters. @@ -1431,7 +1431,7 @@ def align_dld_sectors( """Aligns the time-of-flight axis of the different sections of a detector. Args: - df (Union[pd.DataFrame, dask.dataframe.DataFrame]): Dataframe to use. + df (dask.dataframe.DataFrame): Dataframe to use. tof_column (str, optional): Name of the column containing the time-of-flight values. Defaults to config["dataframe"]["tof_column"]. sector_id_column (str, optional): Name of the column containing the sector id values. @@ -1440,8 +1440,8 @@ def align_dld_sectors( config["dataframe"]["sector_delays"]. Returns: - dask.dataframe.DataFrame: Dataframe with the new columns. - dict: Metadata dictionary. + tuple[dask.dataframe.DataFrame, dict]: Dataframe with the new columns and Metadata + dictionary. """ if sector_delays is None: sector_delays = self.sector_delays @@ -1647,7 +1647,7 @@ def extract_bias(files: list[str], bias_key: str) -> np.ndarray: """Read bias values from hdf5 files Args: - files (List[str]): List of filenames + files (list[str]): List of filenames bias_key (str): hdf5 path to the bias value Returns: @@ -1679,7 +1679,7 @@ def correction_function( y (float | np.ndarray): y coordinate correction_type (str): type of correction. One of "spherical", "Lorentzian", "Gaussian", or "Lorentzian_asymmetric" - center (Tuple[int, int]): center position of the distribution (x,y) + center (tuple[int, int]): center position of the distribution (x,y) amplitude (float): Amplitude of the correction **kwds: Keyword arguments: @@ -1853,13 +1853,13 @@ def range_convert( Args: x (np.ndarray): Values of the x axis (e.g. time-of-flight values). - xrng (Tuple): Boundary value range on the x axis. + xrng (tuple): Boundary value range on the x axis. pathcorr (np.ndarray): Path correspondence between two 1D arrays in the following form, [(id_1_trace_1, id_1_trace_2), (id_2_trace_1, id_2_trace_2), ...] Returns: - Tuple: Transformed range according to the path correspondence. + tuple: Transformed range according to the path correspondence. """ pathcorr = np.asarray(pathcorr) xrange_trans = [] @@ -1898,7 +1898,7 @@ def peaksearch( Args: traces (np.ndarray): Collection of 1D spectra. tof (np.ndarray): Time-of-flight values. - ranges (List[Tuple], optional): List of ranges for peak detection in the format + ranges (list[tuple], optional): List of ranges for peak detection in the format [(LowerBound1, UpperBound1), (LowerBound2, UpperBound2), ....]. Defaults to None. pkwindow (int, optional): Window width of a peak (amounts to lookahead in @@ -1948,7 +1948,7 @@ def _datacheck_peakdetect( ValueError: Raised if x and y values don't have the same length. Returns: - Tuple[np.ndarray, np.ndarray]: Tuple of checked (x/y) arrays. + tuple[np.ndarray, np.ndarray]: Tuple of checked (x/y) arrays. """ if x_axis is None: @@ -1998,7 +1998,7 @@ def peakdetect1d( ValueError: Raised if lookahead and delta are out of range. Returns: - Tuple[np.ndarray, np.ndarray]: Tuple of positions of the positive peaks, + tuple[np.ndarray, np.ndarray]: Tuple of positions of the positive peaks, positions of the negative peaks """ max_peaks = [] From 0bc133587be275abc5ace758c43a00d2493e09e0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 22 Jun 2024 17:31:29 +0200 Subject: [PATCH 086/300] allow testing_multiversion.yml to run on v1 branch --- .github/workflows/testing_multiversion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index 896368a4..2495df8a 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -4,7 +4,7 @@ name: Unit Tests on: workflow_dispatch: push: - branches: [ main ] + branches: [ main, v1_feature_branch ] paths-ignore: pyproject.toml From b0644e82b11d6a7284a541ada56ed7f1b73f988b Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 22 Jun 2024 21:29:32 +0200 Subject: [PATCH 087/300] try limiting python 3.12 version --- .github/workflows/testing_multiversion.yml | 2 +- poetry.lock | 28 +++++++++++----------- pyproject.toml | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index 2495df8a..669f7e81 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -13,7 +13,7 @@ jobs: # Using matrix strategy strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12.2"] runs-on: ubuntu-latest steps: # Check out repo and set up Python diff --git a/poetry.lock b/poetry.lock index 55f8988c..74815b52 100644 --- a/poetry.lock +++ b/poetry.lock @@ -995,13 +995,13 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.15.3" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.3-py3-none-any.whl", hash = "sha256:0151273e5b5d6cf753a61ec83b3a9b7d8821c39ae9af9d7ecf2f9e2f17404103"}, - {file = "filelock-3.15.3.tar.gz", hash = "sha256:e1199bf5194a2277273dacd50269f0d87d0682088a3c561c15674ea9005d8635"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] @@ -4269,13 +4269,13 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools [[package]] name = "sphinx-autodoc-typehints" -version = "2.2.1" +version = "2.2.2" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false python-versions = ">=3.9" files = [ - {file = "sphinx_autodoc_typehints-2.2.1-py3-none-any.whl", hash = "sha256:ac37852861c58a5ca95be13d5a0f49f3661b5341eaf7de8531842135600aeb90"}, - {file = "sphinx_autodoc_typehints-2.2.1.tar.gz", hash = "sha256:26a81e6444c9b82a952519a3b7c52e45f14a0f81c91cfc7063cfcf2ca109d161"}, + {file = "sphinx_autodoc_typehints-2.2.2-py3-none-any.whl", hash = "sha256:b98337a8530c95b73ba0c65465847a8ab0a13403bdc81294d5ef396bbd1f783e"}, + {file = "sphinx_autodoc_typehints-2.2.2.tar.gz", hash = "sha256:128e600eeef63b722f3d8dac6403594592c8cade3ba66fd11dcb997465ee259d"}, ] [package.dependencies] @@ -4634,13 +4634,13 @@ files = [ [[package]] name = "types-requests" -version = "2.32.0.20240602" +version = "2.32.0.20240622" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"}, - {file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"}, + {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"}, + {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"}, ] [package.dependencies] @@ -4718,13 +4718,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.2" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -4961,5 +4961,5 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" -python-versions = ">=3.9, <3.13" -content-hash = "5375409c845ef46117599d89d10fec86560fa1ceaaab7d2f0e9bbc0f4032db1c" +python-versions = ">=3.9, <3.12.3" +content-hash = "db95a000750305a8743b5758e903e75fb0026608ead249eea7262c7cae53c076" diff --git a/pyproject.toml b/pyproject.toml index 6d98d148..f689711b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ keywords = ["sed", "mpes", "flash", "arpes"] license = "MIT" [tool.poetry.dependencies] -python = ">=3.9, <3.13" +python = ">=3.9, <3.12.3" bokeh = ">=2.4.2" dask = {version = ">=2021.12.0, <2023.12.1"} fastdtw = ">=0.3.4" From 030395c59e616529edc51f297b1ebda3aa5bdea5 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 22 Jun 2024 21:43:41 +0200 Subject: [PATCH 088/300] exclude python 3.11.9 --- .github/workflows/testing_multiversion.yml | 4 ++-- poetry.lock | 4 ++-- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index 669f7e81..e4b44470 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -13,7 +13,7 @@ jobs: # Using matrix strategy strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12.2"] + python-version: ["3.9", "3.10", "3.11.8", "3.12.2"] runs-on: ubuntu-latest steps: # Check out repo and set up Python @@ -26,7 +26,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: ${{matrix.python-version}} - poetry-version: 1.2.2 + poetry-version: 1.8.3 # Use cached python and dependencies, install poetry - name: Run tests on python ${{matrix.python-version}} diff --git a/poetry.lock b/poetry.lock index 74815b52..f1855cef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4961,5 +4961,5 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" -python-versions = ">=3.9, <3.12.3" -content-hash = "db95a000750305a8743b5758e903e75fb0026608ead249eea7262c7cae53c076" +python-versions = ">=3.9, <3.12.3, !=3.11.9" +content-hash = "e2c46628207d2ac571d40b2b191956117af6a078b879e3aad5961c5a982a1a90" diff --git a/pyproject.toml b/pyproject.toml index f689711b..985babda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ keywords = ["sed", "mpes", "flash", "arpes"] license = "MIT" [tool.poetry.dependencies] -python = ">=3.9, <3.12.3" +python = ">=3.9, <3.12.3, !=3.11.9" bokeh = ">=2.4.2" dask = {version = ">=2021.12.0, <2023.12.1"} fastdtw = ">=0.3.4" From 0f28b8d7b72bd562d6c0de42364ae77563724779 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 22 Jun 2024 21:49:45 +0200 Subject: [PATCH 089/300] update poetry version --- .github/workflows/benchmark.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/linting.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/testing_coverage.yml | 2 +- .github/workflows/update_dependencies.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 89e0d708..0dbc97ea 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -29,7 +29,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 # Run benchmakrs - name: Run benchmarks on python 3.8 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 19b9e59b..fd92f324 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -48,7 +48,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 - name: Install notebook dependencies run: poetry install -E notebook --with docs diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f12a5f1e..8ccc0559 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -20,7 +20,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 # Linting steps, excute all linters even if one fails - name: ruff diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75996bee..aed41825 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: uses: zain-sohail/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 working-directory: sed-processor - name: Change to distribution name in toml file @@ -83,7 +83,7 @@ jobs: uses: zain-sohail/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 working-directory: sed-processor - name: Change to distribution name in toml file diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index 692e2ab4..71c86263 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -24,7 +24,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 # Run pytest with coverage report, saving to xml - name: Run tests on python 3.9 diff --git a/.github/workflows/update_dependencies.yml b/.github/workflows/update_dependencies.yml index 166378e0..115ec552 100644 --- a/.github/workflows/update_dependencies.yml +++ b/.github/workflows/update_dependencies.yml @@ -29,7 +29,7 @@ jobs: uses: packetcoders/action-setup-cache-python-poetry@main with: python-version: 3.9 - poetry-version: 1.2.2 + poetry-version: 1.8.3 # update poetry lockfile - name: "Update poetry lock file" From 23c91317d5cf3fa395e39d27e8b2f3a2f24cf54b Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 20:46:59 +0200 Subject: [PATCH 090/300] some additional fixes --- sed/calibrator/momentum.py | 14 ++++++-------- sed/core/processor.py | 36 ++++++++++++++++++------------------ sed/loader/sxp/loader.py | 4 ++-- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 0623ca26..7c009136 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -609,7 +609,7 @@ def spline_warp_estimate( Order of interpolation (see ``scipy.ndimage.map_coordinates()``). Defaults to 1. ascale: (float | list | tuple | np.ndarray, optional): Scale parameter determining a - realtive scale for each symmetry feature. If provided as single float, rotsym has + relative scale for each symmetry feature. If provided as single float, rotsym has to be 4. This parameter describes the relative scaling between the two orthogonal symmetry directions (for an orthorhombic system). This requires the correction points to be located along the principal axes (X/Y points of the Brillouin zone). @@ -1975,14 +1975,12 @@ def dictmerge( Returns: dict: Merged dictionary. """ - if isinstance( - other_entries, - (list, tuple), - ): # Merge main_dict with a list or tuple of dictionaries + # Merge main_dict with a list or tuple of dictionaries + if isinstance(other_entries, (list, tuple)): for oth in other_entries: main_dict = {**main_dict, **oth} - - elif isinstance(other_entries, dict): # Merge D with a single dictionary + # Merge D with a single dictionary + elif isinstance(other_entries, dict): main_dict = {**main_dict, **other_entries} return main_dict @@ -2147,7 +2145,7 @@ def load_dfield(file: str) -> tuple[np.ndarray, np.ndarray]: file (str): Path to file containing the inverse dfield Returns: - np.ndarray: the loaded inverse deformation field + tuple[np.ndarray, np.ndarray]: the loaded inverse row and column deformation fields """ rdeform_field: np.ndarray = None cdeform_field: np.ndarray = None diff --git a/sed/core/processor.py b/sed/core/processor.py index f1632f74..f02ac65c 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1491,16 +1491,16 @@ def add_energy_offset( Args: constant (float, optional): The constant to shift the energy axis by. - columns (str | Sequence[str]): Name of the column(s) to apply the shift from. - weights (float | Sequence[float]): weights to apply to the columns. + columns (str | Sequence[str], optional): Name of the column(s) to apply the shift from. + weights (float | Sequence[float], optional): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - reductions (str | Sequence[str]): The reduction to apply to the column. Should be an - available method of dask.dataframe.Series. For example "mean". In this case the - function is applied to the column to generate a single value for the whole dataset. - If None, the shift is applied per-dataframe-row. Defaults to None. Currently only - "mean" is supported. - preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column - before applying the shift. Defaults to False. + reductions (str | Sequence[str], optional): The reduction to apply to the column. + Should be an available method of dask.dataframe.Series. For example "mean". In this + case the function is applied to the column to generate a single value for the whole + dataset. If None, the shift is applied per-dataframe-row. Defaults to None. + Currently only "mean" is supported. + preserve_mean (bool | Sequence[bool], optional): Whether to subtract the mean of the + column before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. @@ -1826,16 +1826,16 @@ def add_delay_offset( Args: constant (float, optional): The constant to shift the delay axis by. flip_delay_axis (bool, optional): Option to reverse the direction of the delay axis. - columns (str | Sequence[str]): Name of the column(s) to apply the shift from. - weights (float | Sequence[float]): weights to apply to the columns. + columns (str | Sequence[str], optional): Name of the column(s) to apply the shift from. + weights (float | Sequence[float], optional): weights to apply to the columns. Can also be used to flip the sign (e.g. -1). Defaults to 1. - reductions (str | Sequence[str]): The reduction to apply to the column. Should be an - available method of dask.dataframe.Series. For example "mean". In this case the - function is applied to the column to generate a single value for the whole dataset. - If None, the shift is applied per-dataframe-row. Defaults to None. Currently only - "mean" is supported. - preserve_mean (bool | Sequence[bool]): Whether to subtract the mean of the column - before applying the shift. Defaults to False. + reductions (str | Sequence[str], optional): The reduction to apply to the column. + Should be an available method of dask.dataframe.Series. For example "mean". In this + case the function is applied to the column to generate a single value for the whole + dataset. If None, the shift is applied per-dataframe-row. Defaults to None. + Currently only "mean" is supported. + preserve_mean (bool | Sequence[bool], optional): Whether to subtract the mean of the + column before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index b8a7ad29..b165257f 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -934,8 +934,8 @@ def read_dataframe( collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. Returns: - tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame - and metadata. + tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame, + timed DataFrame, and metadata. Raises: ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. From 2e1747c630b4cfc220e741a476be5f61926fdba3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 20:47:13 +0200 Subject: [PATCH 091/300] add setuptools due to broken pynxtools package --- poetry.lock | 129 +++++++++++++++++++++++++++---------------------- pyproject.toml | 3 +- 2 files changed, 74 insertions(+), 58 deletions(-) diff --git a/poetry.lock b/poetry.lock index f1855cef..d3477db8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -713,63 +713,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.5.3" +version = "7.5.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] [package.dependencies] @@ -1272,13 +1272,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.2.0" +version = "7.2.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.2.0-py3-none-any.whl", hash = "sha256:04e4aad329b8b948a5711d394fa8759cb80f009225441b4f2a02bd4d8e5f426c"}, - {file = "importlib_metadata-7.2.0.tar.gz", hash = "sha256:3ff4519071ed42740522d494d04819b666541b9752c43012f85afb2cc220fcc6"}, + {file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"}, + {file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"}, ] [package.dependencies] @@ -4187,6 +4187,21 @@ nativelib = ["pyobjc-framework-Cocoa", "pywin32"] objc = ["pyobjc-framework-Cocoa"] win32 = ["pywin32"] +[[package]] +name = "setuptools" +version = "70.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, + {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -4962,4 +4977,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "e2c46628207d2ac571d40b2b191956117af6a078b879e3aad5961c5a982a1a90" +content-hash = "f9e9bd4610fc23f1199ac939b1e6359bf49c4d19d970f244ece6644e42109bf5" diff --git a/pyproject.toml b/pyproject.toml index 985babda..05f7aeac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9, <3.12.3, !=3.11.9" bokeh = ">=2.4.2" -dask = {version = ">=2021.12.0, <2023.12.1"} +dask = ">=2021.12.0, <2023.12.1" fastdtw = ">=0.3.4" h5py = ">=3.6.0" ipympl = ">=0.9.1" @@ -31,6 +31,7 @@ pynxtools-mpes = ">=0.0.3" pynxtools = ">=0.3.1" pyyaml = ">=6.0.0" scipy = ">=1.8.0" +setuptools = ">70" symmetrize = ">=0.5.5" threadpoolctl = ">=3.1.0" tifffile = ">=2022.2.9" From f2700ede86dc65786fc864f31e6ebdbaf4dee97a Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 21:31:05 +0200 Subject: [PATCH 092/300] add benchmark to v1_feature_branch --- .github/workflows/benchmark.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 0dbc97ea..12ba66ae 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -4,7 +4,7 @@ name: benchmark on: workflow_dispatch: push: - branches: [ main, create-pull-request/patch ] + branches: [ main, v1_feature_branch, create-pull-request/patch ] paths-ignore: pyproject.toml From 45725067b79eff7ad33af90d6a99fb2444e43351 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 22:18:50 +0200 Subject: [PATCH 093/300] reset flash benchmark target --- benchmarks/benchmark_targets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index 20867c96..e6f22f15 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,7 +1,7 @@ binning_1d: 3.823766174933333 binning_4d: 10.874196000533333 inv_dfield: 6.449857629866649 -loader_compute_flash: 0.046021607999970605 +loader_compute_flash: 1 loader_compute_mpes: 0.01723324879997866 loader_compute_sxp: 0.00812402506668756 workflow_1d: 20.176347301066652 From a9e731d4d993b31dc4b15ec52ba284e5a5a8231f Mon Sep 17 00:00:00 2001 From: rettigl <53396064+rettigl@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:26:36 +0000 Subject: [PATCH 094/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index e6f22f15..c7ba4b78 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,8 +1,8 @@ -binning_1d: 3.823766174933333 -binning_4d: 10.874196000533333 +binning_1d: 3.5267653551999953 +binning_4d: 10.209467188266672 inv_dfield: 6.449857629866649 -loader_compute_flash: 1 -loader_compute_mpes: 0.01723324879997866 -loader_compute_sxp: 0.00812402506668756 -workflow_1d: 20.176347301066652 -workflow_4d: 26.607221860799985 +loader_compute_flash: 0.0709010237333132 +loader_compute_mpes: 0.01700571413330181 +loader_compute_sxp: 0.007494217600045279 +workflow_1d: 20.061807410666642 +workflow_4d: 24.679874365600032 From b2b0c3da42eb1f878a76caecced58fb1cc9a7fbc Mon Sep 17 00:00:00 2001 From: rettigl <53396064+rettigl@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:33:31 +0000 Subject: [PATCH 095/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index c7ba4b78..431aa61e 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,8 +1,8 @@ binning_1d: 3.5267653551999953 binning_4d: 10.209467188266672 inv_dfield: 6.449857629866649 -loader_compute_flash: 0.0709010237333132 +loader_compute_flash: 0.06769129813333165 loader_compute_mpes: 0.01700571413330181 -loader_compute_sxp: 0.007494217600045279 +loader_compute_sxp: 0.007209246666676942 workflow_1d: 20.061807410666642 workflow_4d: 24.679874365600032 From e2b0d4fb6e001e89a6eeb27aecc320247a1ee5ce Mon Sep 17 00:00:00 2001 From: rettigl <53396064+rettigl@users.noreply.github.com> Date: Sun, 23 Jun 2024 20:40:19 +0000 Subject: [PATCH 096/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index 431aa61e..e669cf46 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -3,6 +3,6 @@ binning_4d: 10.209467188266672 inv_dfield: 6.449857629866649 loader_compute_flash: 0.06769129813333165 loader_compute_mpes: 0.01700571413330181 -loader_compute_sxp: 0.007209246666676942 +loader_compute_sxp: 0.007175734666649684 workflow_1d: 20.061807410666642 -workflow_4d: 24.679874365600032 +workflow_4d: 24.447920182666667 From 2be3ae609b34da5286e426928a6c332744cb8d82 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 23:53:08 +0200 Subject: [PATCH 097/300] reset flash loader target --- benchmarks/benchmark_targets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index b2b451cb..7dcc5bcf 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,7 +1,7 @@ binning_1d: 3.017609174399999 binning_4d: 9.210316116800005 inv_dfield: 5.196141159999996 -loader_compute_flash: 0.03584787449999567 +loader_compute_flash: 1 loader_compute_mpes: 0.015864623800007395 loader_compute_sxp: 0.006027440450000654 workflow_1d: 17.0553120846 From 0ccbb1cc30fa9620e2ab3bc190febfe11b83ec9f Mon Sep 17 00:00:00 2001 From: rettigl <53396064+rettigl@users.noreply.github.com> Date: Sun, 23 Jun 2024 21:59:00 +0000 Subject: [PATCH 098/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index 7dcc5bcf..95d90c3a 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,7 +1,7 @@ binning_1d: 3.017609174399999 binning_4d: 9.210316116800005 inv_dfield: 5.196141159999996 -loader_compute_flash: 1 +loader_compute_flash: 0.048782521849997804 loader_compute_mpes: 0.015864623800007395 loader_compute_sxp: 0.006027440450000654 workflow_1d: 17.0553120846 From 147e9134c11c5b58f4f14db98fc2e9e52521d48e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 24 Jun 2024 21:39:14 +0200 Subject: [PATCH 099/300] add back annotations --- sed/loader/flash/loader.py | 2 ++ sed/loader/sxp/loader.py | 2 ++ sed/loader/utils.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index cf6efe22..7427ef73 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -7,6 +7,8 @@ This can then be saved as a parquet for out-of-sed processing and reread back to access other sed functionality. """ +from __future__ import annotations + import re import time from collections.abc import Sequence diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 21ad4ae4..4b33d77d 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -9,6 +9,8 @@ sed funtionality. Most of the structure is identical to the FLASH loader. """ +from __future__ import annotations + import time from collections.abc import Sequence from functools import reduce diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 37ec6285..7e16720a 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -1,5 +1,7 @@ """Utilities for loaders """ +from __future__ import annotations + from collections.abc import Sequence from glob import glob from pathlib import Path From f2a26b9315df621d97a22a52205ab9a311c08164 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 24 Jun 2024 22:07:34 +0200 Subject: [PATCH 100/300] use index and dataset keys --- sed/config/flash_example_config.yaml | 34 +++++++++++++------- sed/loader/flash/dataframe.py | 14 +++----- tests/data/loader/flash/config.yaml | 27 ++++++++++------ tests/loader/flash/test_buffer_handler.py | 3 +- tests/loader/flash/test_dataframe_creator.py | 24 +++++--------- 5 files changed, 55 insertions(+), 47 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index bfdf0cd3..944605fe 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -99,24 +99,28 @@ dataframe: # The timestamp timeStamp: format: per_train - group_name: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/" + index_key: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/index" + dataset_key: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/time" # pulse ID is a necessary channel for using the loader. pulseId: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 2 # detector x position dldPosX: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 1 # detector y position dldPosY: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 0 # Detector time-of-flight channel @@ -124,14 +128,16 @@ dataframe: # also the dldSectorID channel dldTimeSteps: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 3 # The auxillary channel has a special structure where the group further contains # a multidim structure so further aliases are defined below dldAux: format: per_pulse - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 dldAuxChannels: sampleBias: 0 @@ -145,29 +151,35 @@ dataframe: # ADC containing the pulser sign (1: value approx. 35000, 0: 33000) pulserSignAdc: format: per_pulse - group_name: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/" + index_key: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/index" + dataset_key: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/value" # the energy of the monochromatized beam. This is a quasi-static value. # there is a better channel which still needs implementation. monochromatorPhotonEnergy: format: per_train - group_name: "/FL1/Beamlines/PG/Monochromator/monochromator photon energy/" + index_key: "/FL1/Beamlines/PG/Monochromator/monochromator photon energy/index" + dataset_key: "/FL1/Beamlines/PG/Monochromator/monochromator photon energy/value" # The GMDs can not be read yet... gmdBda: format: per_train - group_name: "/FL1/Photon Diagnostic/GMD/Average energy/energy BDA/" + index_key: "/FL1/Photon Diagnostic/GMD/Average energy/energy BDA/index" + dataset_key: "/FL1/Photon Diagnostic/GMD/Average energy/energy BDA/value" + # Beam Arrival Monitor, vital for pump-probe experiments as it can compensate sase # timing fluctuations. # Here we use the DBC2 BAM as the "normal" one is broken. bam: format: per_pulse - group_name: "/uncategorised/FLASH.SDIAG/BAM.DAQ/FL0.DBC2.ARRIVAL_TIME.ABSOLUTE.SA1.COMP/" + index_key: "/uncategorised/FLASH.SDIAG/BAM.DAQ/FL0.DBC2.ARRIVAL_TIME.ABSOLUTE.SA1.COMP/index" + dataset_key: "/uncategorised/FLASH.SDIAG/BAM.DAQ/FL0.DBC2.ARRIVAL_TIME.ABSOLUTE.SA1.COMP/value" # The delay Stage position, encoding the pump-probe delay delayStage: format: per_train - group_name: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/" + index_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/index" + dataset_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/value" # The prefixes of the stream names for different DAQ systems for parsing filenames # (Not to be changed by user) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 31d71e47..91dedf1e 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -40,25 +40,19 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: tuple[str, str]: Outputs a tuple of 'index_key' and 'dataset_key'. Raises: - ValueError: If neither 'group_name' nor both 'index_key' and 'dataset_key' are provided. + ValueError: If 'index_key' and 'dataset_key' are not provided. """ channel_config = self._config["channels"][channel] - if "group_name" in channel_config: - index_key = channel_config["group_name"] + "index" - if channel == "timeStamp": - dataset_key = channel_config["group_name"] + "time" - else: - dataset_key = channel_config["group_name"] + "value" - return index_key, dataset_key if "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] + else: + print("'group_name' is no longer supported.") raise ValueError( "For channel:", channel, - "Provide either both 'index_key' and 'dataset_key'.", - "or 'group_name' (parses only 'index' and 'value' or 'time' keys.)", + "Provide both 'index_key' and 'dataset_key'.", ) def get_dataset_array( diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 0d8d1c93..652e875c 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -81,24 +81,28 @@ dataframe: # pulse ID is a necessary channel for using the loader. pulseId: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 2 dldPosX: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 1 dtype: uint16 dldPosY: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 0 dtype: uint16 dldTimeSteps: format: per_electron - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 3 dtype: uint32 @@ -106,7 +110,8 @@ dataframe: # a multidim structure so further aliases are defined below dldAux: format: per_train - group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" + index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" + dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 dldAuxChannels: sampleBias: 0 @@ -119,19 +124,23 @@ dataframe: timeStamp: format: per_train - group_name: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/" + index_key: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/index" + dataset_key: "/uncategorised/FLASH.DIAG/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/time" delayStage: format: per_train - group_name: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/" + index_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/index" + dataset_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/value" pulserSignAdc: format: per_pulse - group_name: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/" + index_key: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/index" + dataset_key: "/FL1/Experiment/PG/SIS8300 100MHz ADC/CH6/TD/value" gmdTunnel: format: per_pulse - group_name: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/" + index_key: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index" + dataset_key: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/value" slice: 0 # The prefixes of the stream names for different DAQ systems for parsing filenames diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index eb7666c9..822e3c04 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -73,7 +73,8 @@ def test_buffer_schema_mismatch(config, h5_paths): # Manipulate the configuration to introduce a new channel 'gmdTunnel2' config_dict = config config_dict["dataframe"]["channels"]["gmdTunnel2"] = { - "group_name": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/", + "index_key": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index", + "dataset_key": "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/value", "format": "per_pulse", "slice": 0, } diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 13ecb851..3fbe37ca 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -16,12 +16,11 @@ def test_get_index_dataset_key(config_dataframe, h5_file): channel = "dldPosX" df = DataFrameCreator(config, h5_file) index_key, dataset_key = df.get_index_dataset_key(channel) - group_name = config["channels"][channel]["group_name"] - assert index_key == group_name + "index" - assert dataset_key == group_name + "value" + assert index_key == config["channels"][channel]["index_key"] + assert dataset_key == config["channels"][channel]["dataset_key"] - # remove group_name key - del config["channels"][channel]["group_name"] + # remove index_key + del config["channels"][channel]["index_key"] with pytest.raises(ValueError): df.get_index_dataset_key(channel) @@ -58,13 +57,11 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): df = DataFrameCreator(config_dataframe, h5_file) train_id, dset = df.get_dataset_array(channel) - channel_index_key = config_dataframe["channels"][channel]["group_name"] + "index" + channel_index_key = "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index" # channel_dataset_key = config_dataframe["channels"][channel]["group_name"] + "value" - empty_dataset_key = config_dataframe["channels"][channel]["group_name"] + "empty" + empty_dataset_key = "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/empty" config_dataframe["channels"][channel]["index_key"] = channel_index_key config_dataframe["channels"][channel]["dataset_key"] = empty_dataset_key - # Remove the 'group_name' key - del config_dataframe["channels"][channel]["group_name"] # create an empty dataset h5_file_copy.create_dataset( @@ -237,7 +234,7 @@ def test_group_name_not_in_h5(config_dataframe, h5_file): """Test ValueError when the group_name for a channel does not exist in the H5 file.""" channel = "dldPosX" config = config_dataframe - config["channels"][channel]["group_name"] = "foo" + config["channels"][channel]["dataset_key"] = "foo" df = DataFrameCreator(config, h5_file) with pytest.raises(KeyError): @@ -261,12 +258,7 @@ def test_get_index_dataset_key_error(config_dataframe, h5_file): config = config_dataframe channel = "dldPosX" df = DataFrameCreator(config, h5_file) - index_key, dataset_key = df.get_index_dataset_key(channel) - group_name = config["channels"][channel]["group_name"] - assert index_key == group_name + "index" - assert dataset_key == group_name + "value" - # remove group_name key - del config["channels"][channel]["group_name"] + del config["channels"][channel]["dataset_key"] with pytest.raises(ValueError): df.get_index_dataset_key(channel) From 92afdd983df06126c5adbfb149bae9b1fd25d031 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 24 Jun 2024 22:38:42 +0200 Subject: [PATCH 101/300] update lockfile --- poetry.lock | 237 +++++++++++++++++++++++++--------------------------- 1 file changed, 112 insertions(+), 125 deletions(-) diff --git a/poetry.lock b/poetry.lock index d3477db8..256e49e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -202,70 +202,47 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "astropy" -version = "6.0.1" +version = "5.3.2" description = "Astronomy and astrophysics core library" optional = false python-versions = ">=3.9" files = [ - {file = "astropy-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445"}, - {file = "astropy-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a"}, - {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466"}, - {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88"}, - {file = "astropy-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1"}, - {file = "astropy-6.0.1-cp310-cp310-win32.whl", hash = "sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1"}, - {file = "astropy-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c"}, - {file = "astropy-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23"}, - {file = "astropy-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292"}, - {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e"}, - {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363"}, - {file = "astropy-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4"}, - {file = "astropy-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5"}, - {file = "astropy-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27"}, - {file = "astropy-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97"}, - {file = "astropy-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f"}, - {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc"}, - {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165"}, - {file = "astropy-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4"}, - {file = "astropy-6.0.1-cp312-cp312-win32.whl", hash = "sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723"}, - {file = "astropy-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3"}, - {file = "astropy-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9"}, - {file = "astropy-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6"}, - {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08"}, - {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf"}, - {file = "astropy-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0"}, - {file = "astropy-6.0.1-cp39-cp39-win32.whl", hash = "sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563"}, - {file = "astropy-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23"}, - {file = "astropy-6.0.1.tar.gz", hash = "sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c"}, + {file = "astropy-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4ece84db9f03342b80d1af38602ea41e12a7deaf7ebdc6d96514a6fc58827ee"}, + {file = "astropy-5.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32df08df5bcf1fc90d40f7c892e893390a1827b41edc813419b7356ba7379841"}, + {file = "astropy-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6824fd17922c162fd4adadd0cfdb7e1039a8a56d6a9017b96cc1daf2b4a3ea14"}, + {file = "astropy-5.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddfd0d751a72e7a64110c973834d972cab86e567a49009491ab0fc1fb6772816"}, + {file = "astropy-5.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a9b8fbbf7eddc0cd6d65967e62fea69a06cd6985185adccc02ffdf37e256e7d"}, + {file = "astropy-5.3.2-cp310-cp310-win32.whl", hash = "sha256:71057f9117bfaedd732bec02711ee6a32bdad05d59120269f4010317a506069e"}, + {file = "astropy-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:62607f14a4f75f42dbd01f2a7a6c885ed5dc79732a438a0c093b13fed9e58d02"}, + {file = "astropy-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:80565dbd2b069d7d24b3a9b9c50e30a87fea5afb7c22a478fff053ebfb1362ac"}, + {file = "astropy-5.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0a429a6e5225b9209e40130926ad5398fc19564022df09d9e73fe27bc1d03101"}, + {file = "astropy-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e6c19b4592c2d130938d6a91c17057bacfcb9b3fb355c2dfd8c808ec30133c6"}, + {file = "astropy-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d987b6ef0d9ad8c1ad0c40375513f964171a96b7f924a3d5f214f831d0569183"}, + {file = "astropy-5.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:aa64cec74c12befa36a31430ce20570fbdbf8c644907c70430a45fb1de3155ad"}, + {file = "astropy-5.3.2-cp311-cp311-win32.whl", hash = "sha256:44f9ea02acb6a3b520681700cec856f7c7f1a5d9e5ab05de367e6f3f6ac13300"}, + {file = "astropy-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:39ac5d4bad78fbd7596d1279c569a077fd51eb204237c6a92c3d1f7473d4a86c"}, + {file = "astropy-5.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b934394f00c2ba84ba1cf12b0a29c2d5692f95e4f0b2eb28b5ce45830c75887"}, + {file = "astropy-5.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8f8b72e1b1786a5556681ea13d44b8529910d3a9f33bce363217300ed07e3b9"}, + {file = "astropy-5.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24887da8f755d2ce9a72142743ed6cf0ca7006b5ccd7c81c18e515f3556106b6"}, + {file = "astropy-5.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0ccc2df80b56f62a40887271a63030f8aa0c0a044c740b40f54e3cd02645a966"}, + {file = "astropy-5.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b4c9359794226f9805970878c9ae3cb9c2a217cc1a4fa9d52696b0177aba578e"}, + {file = "astropy-5.3.2-cp39-cp39-win32.whl", hash = "sha256:cdcff156cb966e92244f5e282292d2a793a6bc68762cfb0935ba7e5230fffa1d"}, + {file = "astropy-5.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:e1f304b138adfbe5996a45ea57e50de7b1efcd95475df36ecd6ef40efefcdbcc"}, + {file = "astropy-5.3.2.tar.gz", hash = "sha256:222003dedd4d1ad00cf3b3a02747fd59c4fc93a97ddbe6328952916ad5f1558f"}, ] [package.dependencies] -astropy-iers-data = ">=0.2024.2.26.0.28.55" -numpy = ">=1.22,<2" +numpy = ">=1.21" packaging = ">=19.0" -pyerfa = ">=2.0.1.1" +pyerfa = ">=2.0" PyYAML = ">=3.13" [package.extras] -all = ["asdf-astropy (>=0.3)", "astropy[recommended]", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2023.4.0)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2023.4.0)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] -docs = ["Jinja2 (>=3.1.3)", "astropy[recommended]", "pytest (>=7.0)", "sphinx", "sphinx-astropy[confv2] (>=1.9.1)", "sphinx-changelog (>=1.2.0)", "sphinx-design", "tomli"] +all = ["asdf (>=2.10.0)", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2022.8.2)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2022.8.2)", "scipy (>=1.5)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] +docs = ["Jinja2 (>=3.0)", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "pytest (>=7.0)", "scipy (>=1.3)", "sphinx", "sphinx-astropy (>=1.6)", "sphinx-changelog (>=1.2.0)"] recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] -test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"] -test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"] - -[[package]] -name = "astropy-iers-data" -version = "0.2024.6.17.0.31.35" -description = "IERS Earth Rotation and Leap Second tables for the astropy core package" -optional = false -python-versions = ">=3.8" -files = [ - {file = "astropy_iers_data-0.2024.6.17.0.31.35-py3-none-any.whl", hash = "sha256:f4e0b40563813c4297745dd4ec03d80b2cbd6cb29340c8df0534b296cb27e3cf"}, - {file = "astropy_iers_data-0.2024.6.17.0.31.35.tar.gz", hash = "sha256:a6e0dca0985e15dfc4f3fc508bfb29b2b046b59eb9d028416860afa9c63b17eb"}, -] - -[package.extras] -docs = ["pytest"] -test = ["hypothesis", "pytest", "pytest-remotedata"] +test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist"] +test-all = ["coverage[toml]", "ipython (>=4.2)", "objgraph", "pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "sgp4 (>=2.3)", "skyfield (>=1.20)"] [[package]] name = "asttokens" @@ -912,17 +889,6 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] -[[package]] -name = "entrypoints" -version = "0.4" -description = "Discover and load entry points from installed packages." -optional = false -python-versions = ">=3.6" -files = [ - {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, - {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, -] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1229,13 +1195,13 @@ files = [ [[package]] name = "imageio" -version = "2.34.1" +version = "2.34.2" description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." optional = false python-versions = ">=3.8" files = [ - {file = "imageio-2.34.1-py3-none-any.whl", hash = "sha256:408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c"}, - {file = "imageio-2.34.1.tar.gz", hash = "sha256:f13eb76e4922f936ac4a7fec77ce8a783e63b93543d4ea3e40793a6cabd9ac7d"}, + {file = "imageio-2.34.2-py3-none-any.whl", hash = "sha256:a0bb27ec9d5bab36a9f4835e51b21d2cb099e1f78451441f94687ff3404b79f8"}, + {file = "imageio-2.34.2.tar.gz", hash = "sha256:5c0c0ee8faa018a1c42f649b90395dd4d3bb6187c09053a0cd6f1fdd51bbff5e"}, ] [package.dependencies] @@ -1592,27 +1558,26 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.4.9" +version = "8.6.2" description = "Jupyter protocol implementation and client libraries" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"}, - {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"}, + {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, + {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, ] [package.dependencies] -entrypoints = "*" -jupyter-core = ">=4.9.2" -nest-asyncio = ">=1.5.4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" tornado = ">=6.2" -traitlets = "*" +traitlets = ">=5.3" [package.extras] -doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] [[package]] name = "jupyter-console" @@ -2699,13 +2664,13 @@ files = [ [[package]] name = "notebook" -version = "6.5.7" +version = "6.5.4" description = "A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.7" files = [ - {file = "notebook-6.5.7-py3-none-any.whl", hash = "sha256:a6afa9a4ff4d149a0771ff8b8c881a7a73b3835f9add0606696d6e9d98ac1cd0"}, - {file = "notebook-6.5.7.tar.gz", hash = "sha256:04eb9011dfac634fbd4442adaf0a8c27cd26beef831fe1d19faf930c327768e4"}, + {file = "notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f"}, + {file = "notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e"}, ] [package.dependencies] @@ -2713,7 +2678,7 @@ argon2-cffi = "*" ipykernel = "*" ipython-genutils = "*" jinja2 = "*" -jupyter-client = ">=5.3.4,<8" +jupyter-client = ">=5.3.4" jupyter-core = ">=4.6.1" nbclassic = ">=0.4.7" nbconvert = ">=5" @@ -2784,47 +2749,56 @@ numpy = ">=1.22,<2.1" [[package]] name = "numpy" -version = "1.26.4" +version = "2.0.0" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, + {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, + {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, + {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, + {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, + {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, + {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, + {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, + {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, + {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, ] [[package]] @@ -2845,12 +2819,14 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] [[package]] @@ -2959,9 +2935,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3656,6 +3632,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -3663,8 +3640,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -3681,6 +3666,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -3688,6 +3674,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, From 621a563750d054f16995e1125aff2b954c09120c Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 24 Jun 2024 22:53:17 +0200 Subject: [PATCH 102/300] limit numpy --- poetry.lock | 166 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 91 insertions(+), 77 deletions(-) diff --git a/poetry.lock b/poetry.lock index 256e49e3..6c7127fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,47 +202,70 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "astropy" -version = "5.3.2" +version = "6.0.1" description = "Astronomy and astrophysics core library" optional = false python-versions = ">=3.9" files = [ - {file = "astropy-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4ece84db9f03342b80d1af38602ea41e12a7deaf7ebdc6d96514a6fc58827ee"}, - {file = "astropy-5.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32df08df5bcf1fc90d40f7c892e893390a1827b41edc813419b7356ba7379841"}, - {file = "astropy-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6824fd17922c162fd4adadd0cfdb7e1039a8a56d6a9017b96cc1daf2b4a3ea14"}, - {file = "astropy-5.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddfd0d751a72e7a64110c973834d972cab86e567a49009491ab0fc1fb6772816"}, - {file = "astropy-5.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a9b8fbbf7eddc0cd6d65967e62fea69a06cd6985185adccc02ffdf37e256e7d"}, - {file = "astropy-5.3.2-cp310-cp310-win32.whl", hash = "sha256:71057f9117bfaedd732bec02711ee6a32bdad05d59120269f4010317a506069e"}, - {file = "astropy-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:62607f14a4f75f42dbd01f2a7a6c885ed5dc79732a438a0c093b13fed9e58d02"}, - {file = "astropy-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:80565dbd2b069d7d24b3a9b9c50e30a87fea5afb7c22a478fff053ebfb1362ac"}, - {file = "astropy-5.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0a429a6e5225b9209e40130926ad5398fc19564022df09d9e73fe27bc1d03101"}, - {file = "astropy-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e6c19b4592c2d130938d6a91c17057bacfcb9b3fb355c2dfd8c808ec30133c6"}, - {file = "astropy-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d987b6ef0d9ad8c1ad0c40375513f964171a96b7f924a3d5f214f831d0569183"}, - {file = "astropy-5.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:aa64cec74c12befa36a31430ce20570fbdbf8c644907c70430a45fb1de3155ad"}, - {file = "astropy-5.3.2-cp311-cp311-win32.whl", hash = "sha256:44f9ea02acb6a3b520681700cec856f7c7f1a5d9e5ab05de367e6f3f6ac13300"}, - {file = "astropy-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:39ac5d4bad78fbd7596d1279c569a077fd51eb204237c6a92c3d1f7473d4a86c"}, - {file = "astropy-5.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b934394f00c2ba84ba1cf12b0a29c2d5692f95e4f0b2eb28b5ce45830c75887"}, - {file = "astropy-5.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8f8b72e1b1786a5556681ea13d44b8529910d3a9f33bce363217300ed07e3b9"}, - {file = "astropy-5.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24887da8f755d2ce9a72142743ed6cf0ca7006b5ccd7c81c18e515f3556106b6"}, - {file = "astropy-5.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0ccc2df80b56f62a40887271a63030f8aa0c0a044c740b40f54e3cd02645a966"}, - {file = "astropy-5.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b4c9359794226f9805970878c9ae3cb9c2a217cc1a4fa9d52696b0177aba578e"}, - {file = "astropy-5.3.2-cp39-cp39-win32.whl", hash = "sha256:cdcff156cb966e92244f5e282292d2a793a6bc68762cfb0935ba7e5230fffa1d"}, - {file = "astropy-5.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:e1f304b138adfbe5996a45ea57e50de7b1efcd95475df36ecd6ef40efefcdbcc"}, - {file = "astropy-5.3.2.tar.gz", hash = "sha256:222003dedd4d1ad00cf3b3a02747fd59c4fc93a97ddbe6328952916ad5f1558f"}, + {file = "astropy-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445"}, + {file = "astropy-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a"}, + {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466"}, + {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88"}, + {file = "astropy-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1"}, + {file = "astropy-6.0.1-cp310-cp310-win32.whl", hash = "sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1"}, + {file = "astropy-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c"}, + {file = "astropy-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23"}, + {file = "astropy-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292"}, + {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e"}, + {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363"}, + {file = "astropy-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4"}, + {file = "astropy-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5"}, + {file = "astropy-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27"}, + {file = "astropy-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97"}, + {file = "astropy-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f"}, + {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc"}, + {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165"}, + {file = "astropy-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4"}, + {file = "astropy-6.0.1-cp312-cp312-win32.whl", hash = "sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723"}, + {file = "astropy-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3"}, + {file = "astropy-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9"}, + {file = "astropy-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6"}, + {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08"}, + {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf"}, + {file = "astropy-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0"}, + {file = "astropy-6.0.1-cp39-cp39-win32.whl", hash = "sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563"}, + {file = "astropy-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23"}, + {file = "astropy-6.0.1.tar.gz", hash = "sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c"}, ] [package.dependencies] -numpy = ">=1.21" +astropy-iers-data = ">=0.2024.2.26.0.28.55" +numpy = ">=1.22,<2" packaging = ">=19.0" -pyerfa = ">=2.0" +pyerfa = ">=2.0.1.1" PyYAML = ">=3.13" [package.extras] -all = ["asdf (>=2.10.0)", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2022.8.2)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2022.8.2)", "scipy (>=1.5)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] -docs = ["Jinja2 (>=3.0)", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "pytest (>=7.0)", "scipy (>=1.3)", "sphinx", "sphinx-astropy (>=1.6)", "sphinx-changelog (>=1.2.0)"] +all = ["asdf-astropy (>=0.3)", "astropy[recommended]", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2023.4.0)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2023.4.0)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] +docs = ["Jinja2 (>=3.1.3)", "astropy[recommended]", "pytest (>=7.0)", "sphinx", "sphinx-astropy[confv2] (>=1.9.1)", "sphinx-changelog (>=1.2.0)", "sphinx-design", "tomli"] recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] -test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist"] -test-all = ["coverage[toml]", "ipython (>=4.2)", "objgraph", "pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "sgp4 (>=2.3)", "skyfield (>=1.20)"] +test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"] +test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"] + +[[package]] +name = "astropy-iers-data" +version = "0.2024.6.24.0.31.11" +description = "IERS Earth Rotation and Leap Second tables for the astropy core package" +optional = false +python-versions = ">=3.8" +files = [ + {file = "astropy_iers_data-0.2024.6.24.0.31.11-py3-none-any.whl", hash = "sha256:0b3799034b0b76af8f915ef822d38cc90e00e235db0cb688018e4f567a8babb9"}, + {file = "astropy_iers_data-0.2024.6.24.0.31.11.tar.gz", hash = "sha256:ef0197b7b84dea248031e553687ea1dc58d7ac9473043693b2d33b46d81a9a12"}, +] + +[package.extras] +docs = ["pytest"] +test = ["hypothesis", "pytest", "pytest-remotedata"] [[package]] name = "asttokens" @@ -2749,56 +2772,47 @@ numpy = ">=1.22,<2.1" [[package]] name = "numpy" -version = "2.0.0" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, - {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, - {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, - {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, - {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, - {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, - {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, - {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, - {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, - {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, - {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, - {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, - {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, - {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, - {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, - {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, - {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, - {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, - {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, - {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, - {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, - {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, - {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, - {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, - {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, - {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, - {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, - {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, - {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, - {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, - {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -4964,4 +4978,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "f9e9bd4610fc23f1199ac939b1e6359bf49c4d19d970f244ece6644e42109bf5" +content-hash = "f8b41a3e994c5b654366626a57e5a71c522829cb188d60c97a2d7ad2937a67ad" diff --git a/pyproject.toml b/pyproject.toml index 480cf644..4c8a7aa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ lmfit = ">=1.0.3" matplotlib = ">=3.5.1" natsort = ">=8.1.0" numba = ">=0.55.1" -numpy = ">=1.18" +numpy = ">=1.18, <2.0" pandas = ">=1.4.1" psutil = ">=5.9.0" pynxtools-mpes = ">=0.0.3" From 3463037f9ef5ee8e44f84cac6edb8d194350dd60 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 24 Jun 2024 22:59:20 +0200 Subject: [PATCH 103/300] install package in benchmark --- .github/workflows/benchmark.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 12ba66ae..b0e4ff98 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,6 +18,9 @@ jobs: with: lfs: true + - name: Install project dependencies + run: poetry install + - uses: tibdex/github-app-token@v1 id: generate-token with: From 7c92427a23347618a354c8a50a967fc31923b4ff Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 24 Jun 2024 23:02:50 +0200 Subject: [PATCH 104/300] fix workflow --- .github/workflows/benchmark.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index b0e4ff98..367f68c8 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,9 +18,6 @@ jobs: with: lfs: true - - name: Install project dependencies - run: poetry install - - uses: tibdex/github-app-token@v1 id: generate-token with: @@ -34,6 +31,9 @@ jobs: python-version: 3.9 poetry-version: 1.8.3 + - name: Install project dependencies + run: poetry install + # Run benchmakrs - name: Run benchmarks on python 3.8 run: | From ad4a3136666579f144c647f1c43de5db6a86a5e7 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 31 May 2024 00:03:34 +0200 Subject: [PATCH 105/300] remove bias energy from energy calibration --- sed/calibrator/energy.py | 68 +++++++++++++--------------------------- sed/core/processor.py | 13 +++----- 2 files changed, 26 insertions(+), 55 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 07ec39bc..cf37d998 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -4,7 +4,6 @@ from __future__ import annotations import itertools as it -import warnings as wn from collections.abc import Sequence from copy import deepcopy from datetime import datetime @@ -512,7 +511,7 @@ def feature_extract( def calibrate( self, - ref_id: int = 0, + ref_energy: float = 0, method: str = "lmfit", energy_scale: str = "kinetic", landmarks: np.ndarray = None, @@ -525,8 +524,7 @@ def calibrate( scale using optimization methods. Args: - ref_id (int, optional): The reference trace index (an integer). - Defaults to 0. + ref_energy (float): Binding/kinetic energy of the detected feature. method (str, optional): Method for determining the energy calibration. - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form. @@ -581,7 +579,7 @@ def calibrate( sign * biases, binwidth, binning, - ref_id=ref_id, + ref_energy=ref_energy, t=t, energy_scale=energy_scale, verbose=verbose, @@ -591,7 +589,7 @@ def calibrate( self.calibration = poly_energy_calibration( landmarks, sign * biases, - ref_id=ref_id, + ref_energy=ref_energy, aug=self.dup, method=method, t=t, @@ -659,7 +657,7 @@ def view( # pylint: disable=dangerous-default-value for itr, trace in enumerate(traces): if align: ax.plot( - xaxis + sign * (self.biases[itr] - self.biases[self.calibration["refid"]]), + xaxis + sign * (self.biases[itr]), trace, ls="-", linewidth=1, @@ -722,7 +720,7 @@ def view( # pylint: disable=dangerous-default-value trace = traces[itr, :] if align: fig.line( - xaxis + sign * (self.biases[itr] - self.biases[self.calibration["refid"]]), + xaxis + sign * (self.biases[itr]), trace, color=color, line_dash="solid", @@ -2089,8 +2087,7 @@ def fit_energy_calibration( vals: list[float] | np.ndarray, binwidth: float, binning: int, - ref_id: int = 0, - ref_energy: float = None, + ref_energy: float, t: list[float] | np.ndarray = None, energy_scale: str = "kinetic", verbose: bool = True, @@ -2107,9 +2104,8 @@ def fit_energy_calibration( each EDC. binwidth (float): Time width of each original TOF bin in ns. binning (int): Binning factor of the TOF values. - ref_id (int, optional): Reference dataset index. Defaults to 0. - ref_energy (float, optional): Energy value of the feature in the refence - trace (eV). required to output the calibration. Defaults to None. + ref_energy (float): Energy value of the feature in the refence + trace (eV). t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. energy_scale (str, optional): Direction of increasing energy scale. @@ -2134,14 +2130,6 @@ def fit_energy_calibration( - "axis": Fitted energy axis. """ vals = np.asarray(vals) - nvals = vals.size - - if ref_id >= nvals: - wn.warn( - "Reference index (refid) cannot be larger than the number of traces!\ - Reset to the largest allowed number.", - ) - ref_id = nvals - 1 def residual(pars, time, data, binwidth, binning, energy_scale): model = tof2ev( @@ -2210,12 +2198,11 @@ def residual(pars, time, data, binwidth, binning, energy_scale): ecalibdict["t0"] = result.params["t0"].value ecalibdict["E0"] = result.params["E0"].value ecalibdict["energy_scale"] = energy_scale + energy_offset = pfunc(-1 * ref_energy, pos[0]) + ecalibdict["E0"] = -(energy_offset - vals[0]) - if (ref_energy is not None) and (t is not None): - energy_offset = pfunc(-1 * ref_energy, pos[ref_id]) - ecalibdict["axis"] = pfunc(-energy_offset, t) - ecalibdict["E0"] = -energy_offset - ecalibdict["refid"] = ref_id + if t is not None: + ecalibdict["axis"] = pfunc(ecalibdict["E0"], t) return ecalibdict @@ -2223,9 +2210,8 @@ def residual(pars, time, data, binwidth, binning, energy_scale): def poly_energy_calibration( pos: list[float] | np.ndarray, vals: list[float] | np.ndarray, + ref_energy: float, order: int = 3, - ref_id: int = 0, - ref_energy: float = None, t: list[float] | np.ndarray = None, aug: int = 1, method: str = "lstsq", @@ -2245,10 +2231,9 @@ def poly_energy_calibration( (e.g. peaks) in the EDCs. vals (list[float] | np.ndarray): Bias voltage value associated with each EDC. + ref_energy (float): Energy value of the feature in the refence + trace (eV). order (int, optional): Polynomial order of the fitting function. Defaults to 3. - ref_id (int, optional): Reference dataset index. Defaults to 0. - ref_energy (float, optional): Energy value of the feature in the refence - trace (eV). required to output the calibration. Defaults to None. t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. aug (int, optional): Fitting dimension augmentation @@ -2276,21 +2261,14 @@ def poly_energy_calibration( vals = np.asarray(vals) nvals = vals.size - if ref_id >= nvals: - wn.warn( - "Reference index (refid) cannot be larger than the number of traces!\ - Reset to the largest allowed number.", - ) - ref_id = nvals - 1 - # Top-to-bottom ordering of terms in the T matrix - termorder = np.delete(range(0, nvals, 1), ref_id) + termorder = np.delete(range(0, nvals, 1), 0) termorder = np.tile(termorder, aug) # Left-to-right ordering of polynomials in the T matrix polyorder = np.linspace(order, 1, order, dtype="int") # Construct the T (differential drift time) matrix, Tmat = Tmain - Tsec - t_main = np.array([pos[ref_id] ** p for p in polyorder]) + t_main = np.array([pos[0] ** p for p in polyorder]) # Duplicate to the same order as the polynomials t_main = np.tile(t_main, (aug * (nvals - 1), 1)) @@ -2302,7 +2280,7 @@ def poly_energy_calibration( t_mat = t_main - np.asarray(t_sec) # Construct the b vector (differential bias) - bvec = vals[ref_id] - np.delete(vals, ref_id) + bvec = vals[0] - np.delete(vals, 0) bvec = np.tile(bvec, aug) # Solve for the a vector (polynomial coefficients) using least squares @@ -2322,12 +2300,10 @@ def poly_energy_calibration( ecalibdict["Tmat"] = t_mat ecalibdict["bvec"] = bvec ecalibdict["energy_scale"] = energy_scale + ecalibdict["E0"] = -(pfunc(-1 * ref_energy, pos[0]) + vals[0]) - if ref_energy is not None and t is not None: - energy_offset = pfunc(-1 * ref_energy, pos[ref_id]) - ecalibdict["axis"] = pfunc(-energy_offset, t) - ecalibdict["E0"] = -energy_offset - ecalibdict["refid"] = ref_id + if t is not None: + ecalibdict["axis"] = pfunc(-ecalibdict["E0"], t) return ecalibdict diff --git a/sed/core/processor.py b/sed/core/processor.py index f02ac65c..d1bd6a6a 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1289,7 +1289,6 @@ def find_bias_peaks( # 3. Fit the energy calibration relation def calibrate_energy_axis( self, - ref_id: int, ref_energy: float, method: str = None, energy_scale: str = None, @@ -1302,10 +1301,7 @@ def calibrate_energy_axis( approximation, and a d^2/(t-t0)^2 relation. Args: - ref_id (int): id of the trace at the bias where the reference energy is - given. - ref_energy (float): Absolute energy of the detected feature at the bias - of ref_id + ref_energy (float): Binding/kinetic energy of the detected feature. method (str, optional): Method for determining the energy calibration. - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form. @@ -1332,7 +1328,6 @@ def calibrate_energy_axis( energy_scale = self._config["energy"]["energy_scale"] self.ec.calibrate( - ref_id=ref_id, ref_energy=ref_energy, method=method, energy_scale=energy_scale, @@ -1350,7 +1345,7 @@ def calibrate_energy_axis( ) print("E/TOF relationship:") self.ec.view( - traces=self.ec.calibration["axis"][None, :], + traces=self.ec.calibration["axis"][None, :] + self.ec.biases[0], xaxis=self.ec.tof, backend="matplotlib", show_legend=False, @@ -1358,14 +1353,14 @@ def calibrate_energy_axis( if energy_scale == "kinetic": plt.scatter( self.ec.peaks[:, 0], - -(self.ec.biases - self.ec.biases[ref_id]) + ref_energy, + -(self.ec.biases - self.ec.biases[0]) + ref_energy, s=50, c="k", ) elif energy_scale == "binding": plt.scatter( self.ec.peaks[:, 0], - self.ec.biases - self.ec.biases[ref_id] + ref_energy, + self.ec.biases - self.ec.biases[0] + ref_energy, s=50, c="k", ) From 3c22e539289e4522b6aef718b11a457ff7f722d8 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 31 May 2024 22:21:17 +0200 Subject: [PATCH 106/300] unify config interfaces for channels --- sed/config/mpes_example_config.yaml | 27 +++-- sed/loader/mpes/loader.py | 166 +++++++++++++++------------- 2 files changed, 106 insertions(+), 87 deletions(-) diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 95f93218..90f870a5 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -15,14 +15,6 @@ core: gid: 1001 dataframe: - # hdf5 group names to read from the h5 files (for mpes reader) - hdf5_groupnames: ["Stream_0", "Stream_1", "Stream_2", "Stream_4"] - # aliases to assign to the dataframe columns for the corresponding hdf5 streams - hdf5_aliases: - Stream_0: "X" - Stream_1: "Y" - Stream_2: "t" - Stream_4: "ADC" # dataframe column name for the time stamp column time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) @@ -79,6 +71,25 @@ dataframe: kx: '1/A' ky: '1/A' + # dataframe channels and group names to read from the h5 files + channels: + # The timestamp + X: + format: per_electron + group_name: "/Stream_0" + Y: + format: per_electron + group_name: "/Stream_1" + t: + format: per_electron + group_name: "/Stream_2" + ADC: + format: per_electron + group_name: "/Stream_4" + bias_voltage: + format: per_file + group_name: "KTOF:Lens:TOF:VSet" + energy: # Number of bins to use for energy calibration traces bins: 1000 diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c28b32f3..d0b3c9b3 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -9,6 +9,7 @@ import glob import json import os +from typing import Any from collections.abc import Sequence from urllib.error import HTTPError from urllib.error import URLError @@ -27,8 +28,7 @@ def hdf5_to_dataframe( files: Sequence[str], - group_names: Sequence[str] = None, - alias_dict: dict[str, str] = None, + channels: dict[str, Any] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", ms_markers_group: str = "msMarkers", @@ -40,12 +40,9 @@ def hdf5_to_dataframe( Args: files (List[str]): A list of the file paths to load. - group_names (List[str], optional): hdf5 group names to load. Defaults to load - all groups containing "Stream" - alias_dict (dict[str, str], optional): Dictionary of aliases for the dataframe - columns. Keys are the hdf5 groupnames, and values the aliases. If an alias - is not found, its group name is used. Defaults to read the attribute - "Name" from each group. + channels (dict[str, str], optional): hdf5 channels names to load. Each entry in the dict + should contain the keys "format" and "groupName". Defaults to load all groups + containing "Stream", and to read the attribute "Name" from each group. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. time_stamp_alias (str): Alias name for the timestamp column. @@ -58,28 +55,25 @@ def hdf5_to_dataframe( Returns: ddf.DataFrame: The delayed Dask DataFrame """ - if group_names is None: - group_names = [] - if alias_dict is None: - alias_dict = {} - # Read a file to parse the file structure test_fid = kwds.pop("test_fid", 0) test_proc = h5py.File(files[test_fid]) - if group_names == []: - group_names, alias_dict = get_groups_and_aliases( + + if channels is None: + channels = get_groups_and_aliases( h5file=test_proc, seach_pattern="Stream", ) - column_names = [alias_dict.get(group, group) for group in group_names] + channel_list = [channel for channel in channels.values()] + column_names = [name for name in channels.keys()] if time_stamps: column_names.append(time_stamp_alias) test_array = hdf5_to_array( h5file=test_proc, - group_names=group_names, + channels=channel_list, time_stamps=time_stamps, ms_markers_group=ms_markers_group, first_event_time_stamp_key=first_event_time_stamp_key, @@ -90,7 +84,7 @@ def hdf5_to_dataframe( da.from_delayed( dask.delayed(hdf5_to_array)( h5file=h5py.File(f), - group_names=group_names, + channels=channel_list, time_stamps=time_stamps, ms_markers_group=ms_markers_group, first_event_time_stamp_key=first_event_time_stamp_key, @@ -107,8 +101,7 @@ def hdf5_to_dataframe( def hdf5_to_timed_dataframe( files: Sequence[str], - group_names: Sequence[str] = None, - alias_dict: dict[str, str] = None, + channels: dict[str, Any] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", ms_markers_group: str = "msMarkers", @@ -121,12 +114,9 @@ def hdf5_to_timed_dataframe( Args: files (List[str]): A list of the file paths to load. - group_names (List[str], optional): hdf5 group names to load. Defaults to load - all groups containing "Stream" - alias_dict (dict[str, str], optional): Dictionary of aliases for the dataframe - columns. Keys are the hdf5 groupnames, and values the aliases. If an alias - is not found, its group name is used. Defaults to read the attribute - "Name" from each group. + channels (dict[str, str], optional): hdf5 channels names to load. Each entry in the dict + should contain the keys "format" and "groupName". Defaults to load all groups + containing "Stream", and to read the attribute "Name" from each group. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. time_stamp_alias (str): Alias name for the timestamp column. @@ -139,28 +129,25 @@ def hdf5_to_timed_dataframe( Returns: ddf.DataFrame: The delayed Dask DataFrame """ - if group_names is None: - group_names = [] - if alias_dict is None: - alias_dict = {} - # Read a file to parse the file structure test_fid = kwds.pop("test_fid", 0) test_proc = h5py.File(files[test_fid]) - if group_names == []: - group_names, alias_dict = get_groups_and_aliases( + + if channels is None: + channels = get_groups_and_aliases( h5file=test_proc, seach_pattern="Stream", ) - column_names = [alias_dict.get(group, group) for group in group_names] + channel_list = [channel for channel in channels.values()] + column_names = [name for name in channels.keys()] if time_stamps: column_names.append(time_stamp_alias) test_array = hdf5_to_timed_array( h5file=test_proc, - group_names=group_names, + channels=channel_list, time_stamps=time_stamps, ms_markers_group=ms_markers_group, first_event_time_stamp_key=first_event_time_stamp_key, @@ -171,7 +158,7 @@ def hdf5_to_timed_dataframe( da.from_delayed( dask.delayed(hdf5_to_timed_array)( h5file=h5py.File(f), - group_names=group_names, + channels=channel_list, time_stamps=time_stamps, ms_markers_group=ms_markers_group, first_event_time_stamp_key=first_event_time_stamp_key, @@ -190,7 +177,7 @@ def get_groups_and_aliases( h5file: h5py.File, seach_pattern: str = None, alias_key: str = "Name", -) -> tuple[list[str], dict[str, str]]: +) -> dict[str, Any]: """Read groups and aliases from a provided hdf5 file handle Args: @@ -202,8 +189,8 @@ def get_groups_and_aliases( Attribute key where aliases are stored. Defaults to "Name". Returns: - tuple[list[str], dict[str, str]]: - The list of groupnames and the alias dictionary parsed from the file + dict[str, Any]: + A dict of aliases and groupnames parsed from the file """ # get group names: group_names = list(h5file) @@ -218,13 +205,15 @@ def get_groups_and_aliases( for name in filtered_group_names: alias_dict[name] = get_attribute(h5file[name], alias_key) - return filtered_group_names, alias_dict + return { + alias_dict[name]: {"format": "per_electron", "group_name": name} + for name in filtered_group_names + } def hdf5_to_array( h5file: h5py.File, - group_names: Sequence[str], - data_type: str = "float32", + channels: Sequence[Dict[str, Any]], time_stamps=False, ms_markers_group: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", @@ -235,10 +224,8 @@ def hdf5_to_array( Args: h5file (h5py.File): hdf5 file handle to read from - group_names (str): - group names to read - data_type (str, optional): - Data type of the output data. Defaults to "float32". + electron_channels (Sequence[Dict[str, any]]): + channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. ms_markers_group (str): h5 column containing timestamp information. @@ -252,18 +239,39 @@ def hdf5_to_array( # Delayed array for loading an HDF5 file of reasonable size (e.g. < 1GB) + # determine group length from per_electron column: + nelectrons = 0 + for channel in channels: + if channel["format"] == "per_electron": + nelectrons = len(h5file[channel["group_name"]]) + break + if nelectrons == 0: + raise ValueError("No 'per_electron' columns defined, or no hits found in file.") + # Read out groups: data_list = [] - for group in group_names: - g_dataset = np.asarray(h5file[group]) - if bool(data_type): - g_dataset = g_dataset.astype(data_type) + for channel in channels: + if channel["format"] == "per_electron": + g_dataset = np.asarray(h5file[channel["group_name"]]) + elif channel["format"] == "per_file": + value = float(get_attribute(h5file, channel["group_name"])) + g_dataset = np.asarray([value] * nelectrons) + else: + raise ValueError( + f"Invalid 'format':{channel['format']} for channel {channel['group_name']}.", + ) + if "data_type" in channel.keys(): + g_dataset = g_dataset.astype(channel["data_type"]) + else: + g_dataset = g_dataset.astype("float32") + if len(g_dataset) != nelectrons: + raise ValueError(f"Inconsistent entries found for channel {channel['group_name']}.") data_list.append(g_dataset) # calculate time stamps if time_stamps: # create target array for time stamps - time_stamp_data = np.zeros(len(data_list[0])) + time_stamp_data = np.zeros(nelectrons) # the ms marker contains a list of events that occurred at full ms intervals. # It's monotonically increasing, and can contain duplicates ms_marker = np.asarray(h5file[ms_markers_group]) @@ -299,15 +307,14 @@ def hdf5_to_array( start_time + len(ms_marker) / 1000 ) - data_list.append(time_stamp_data) + data_list.append(time_stamp_data.astype("float32")) return np.asarray(data_list) def hdf5_to_timed_array( h5file: h5py.File, - group_names: Sequence[str], - data_type: str = "float32", + channels: Sequence[Dict[str, Any]], time_stamps=False, ms_markers_group: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", @@ -318,10 +325,8 @@ def hdf5_to_timed_array( Args: h5file (h5py.File): hdf5 file handle to read from - group_names (str): - group names to read - data_type (str, optional): - Data type of the output data. Defaults to "float32". + electron_channels (Sequence[Dict[str, any]]): + channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. ms_markers_group (str): h5 column containing timestamp information. @@ -339,14 +344,23 @@ def hdf5_to_timed_array( # Read out groups: data_list = [] ms_marker = np.asarray(h5file[ms_markers_group]) - for group in group_names: - g_dataset = np.asarray(h5file[group]) - if bool(data_type): - g_dataset = g_dataset.astype(data_type) - + for channel in channels: timed_dataset = np.zeros_like(ms_marker) - for i, point in enumerate(ms_marker): - timed_dataset[i] = g_dataset[int(point) - 1] + if channel["format"] == "per_electron": + g_dataset = np.asarray(h5file[channel["group_name"]]) + for i, point in enumerate(ms_marker): + timed_dataset[i] = g_dataset[int(point) - 1] + elif channel["format"] == "per_file": + value = float(get_attribute(h5file, channel["group_name"])) + timed_dataset[:] = value + else: + raise ValueError( + f"Invalid 'format':{channel['format']} for channel {channel['group_name']}.", + ) + if "data_type" in channel.keys(): + timed_dataset = timed_dataset.astype(channel["data_type"]) + else: + timed_dataset = timed_dataset.astype("float32") data_list.append(timed_dataset) @@ -369,7 +383,7 @@ def hdf5_to_timed_array( time_stamp_data = start_time + np.arange(len(ms_marker)) / 1000 - data_list.append(time_stamp_data) + data_list.append(time_stamp_data.astype("float32")) return np.asarray(data_list) @@ -567,13 +581,9 @@ def read_dataframe( metadata=metadata, ) - hdf5_groupnames = kwds.pop( - "hdf5_groupnames", - self._config.get("dataframe", {}).get("hdf5_groupnames", []), - ) - hdf5_aliases = kwds.pop( - "hdf5_aliases", - self._config.get("dataframe", {}).get("hdf5_aliases", {}), + channels = kwds.pop( + "channels", + self._config.get("dataframe", {}).get("channels", []), ) time_stamp_alias = kwds.pop( "time_stamp_alias", @@ -598,8 +608,7 @@ def read_dataframe( ) df = hdf5_to_dataframe( files=self.files, - group_names=hdf5_groupnames, - alias_dict=hdf5_aliases, + channels=channels, time_stamps=time_stamps, time_stamp_alias=time_stamp_alias, ms_markers_group=ms_markers_group, @@ -608,8 +617,7 @@ def read_dataframe( ) timed_df = hdf5_to_timed_dataframe( files=self.files, - group_names=hdf5_groupnames, - alias_dict=hdf5_aliases, + channels=channels, time_stamps=time_stamps, time_stamp_alias=time_stamp_alias, ms_markers_group=ms_markers_group, @@ -680,14 +688,14 @@ def get_start_and_end_time(self) -> tuple[float, float]: h5file = h5py.File(self.files[0]) timestamps = hdf5_to_array( h5file, - group_names=self._config["dataframe"]["hdf5_groupnames"], + channels=self._config["dataframe"]["channels"], time_stamps=True, ) ts_from = timestamps[-1][1] h5file = h5py.File(self.files[-1]) timestamps = hdf5_to_array( h5file, - group_names=self._config["dataframe"]["hdf5_groupnames"], + channels=self._config["dataframe"]["channels"], time_stamps=True, ) ts_to = timestamps[-1][-1] From 508e53dc1a9cc004012a5481237b6cf1ac56c895 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 2 Jun 2024 23:33:46 +0200 Subject: [PATCH 107/300] fix tests and time stamp extraction --- sed/config/mpes_example_config.yaml | 6 ++--- sed/loader/mpes/loader.py | 17 ++++++++++---- tests/calibrator/test_energy.py | 2 -- tests/test_processor.py | 3 --- ...for_example_time-resolved_ARPES_data.ipynb | 23 +++++++++++++++++-- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 90f870a5..4e8a2898 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -86,9 +86,9 @@ dataframe: ADC: format: per_electron group_name: "/Stream_4" - bias_voltage: - format: per_file - group_name: "KTOF:Lens:TOF:VSet" +# bias_voltage: +# format: per_file +# group_name: "KTOF:Lens:Sample:V" energy: # Number of bins to use for energy calibration traces diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index d0b3c9b3..153d9f1a 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -307,7 +307,7 @@ def hdf5_to_array( start_time + len(ms_marker) / 1000 ) - data_list.append(time_stamp_data.astype("float32")) + data_list.append(time_stamp_data) return np.asarray(data_list) @@ -383,7 +383,7 @@ def hdf5_to_timed_array( time_stamp_data = start_time + np.arange(len(ms_marker)) / 1000 - data_list.append(time_stamp_data.astype("float32")) + data_list.append(time_stamp_data) return np.asarray(data_list) @@ -583,7 +583,7 @@ def read_dataframe( channels = kwds.pop( "channels", - self._config.get("dataframe", {}).get("channels", []), + self._config.get("dataframe", {}).get("channels", None), ) time_stamp_alias = kwds.pop( "time_stamp_alias", @@ -686,16 +686,23 @@ def get_start_and_end_time(self) -> tuple[float, float]: tuple[float, float]: A tuple containing the start and end time stamps """ h5file = h5py.File(self.files[0]) + channels = [] + for channel in self._config["dataframe"]["channels"].values(): + if channel["format"] == "per_electron": + channels = [channel] + break + if not channels: + raise ValueError("No valid 'per_electron' channels found.") timestamps = hdf5_to_array( h5file, - channels=self._config["dataframe"]["channels"], + channels=channels, time_stamps=True, ) ts_from = timestamps[-1][1] h5file = h5py.File(self.files[-1]) timestamps = hdf5_to_array( h5file, - channels=self._config["dataframe"]["channels"], + channels=channels, time_stamps=True, ) ts_to = timestamps[-1][-1] diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 0ea4a062..0d0ac344 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -195,11 +195,9 @@ def test_calibrate_append(energy_scale: str, calibration_method: str) -> None: ref_id = 5 ec.add_ranges(ranges=rng, ref_id=ref_id) ec.feature_extract() - refid = 4 e_ref = -0.5 calibdict = ec.calibrate( ref_energy=e_ref, - ref_id=refid, energy_scale=energy_scale, method=calibration_method, ) diff --git a/tests/test_processor.py b/tests/test_processor.py index cffb3a5b..0f10f7fd 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -575,18 +575,15 @@ def test_energy_calibration_workflow(energy_scale: str, calibration_method: str) with pytest.raises(ValueError): processor.calibrate_energy_axis( ref_energy=ref_energy, - ref_id=ref_id, energy_scale="myfantasyscale", ) with pytest.raises(NotImplementedError): processor.calibrate_energy_axis( ref_energy=ref_energy, - ref_id=ref_id, method="myfantasymethod", ) processor.calibrate_energy_axis( ref_energy=ref_energy, - ref_id=ref_id, energy_scale=energy_scale, method=calibration_method, ) diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index af695478..049ace34 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -472,13 +472,12 @@ "source": [ "# use the refid of the bias that the measurement was taken at\n", "# Eref can be used to set the absolute energy (kinetic energy, E-EF) of the feature used for energy calibration (if known)\n", - "refid=4\n", "Eref=-0.5\n", "# the lmfit method uses a fit of (d/(t-t0))**2 to determine the energy calibration\n", "# limits and starting values for the fitting parameters can be provided as dictionaries\n", "sp.calibrate_energy_axis(\n", - " ref_id=refid,\n", " ref_energy=Eref,\n", + " ref_id=0,\n", " method=\"lmfit\",\n", " energy_scale='kinetic',\n", " d={'value':1.0,'min': .7, 'max':1.2, 'vary':True},\n", @@ -538,6 +537,26 @@ "The delay axis is calculated from the ADC input column based on the provided delay range. ALternatively, the delay scan range can also be extracted from attributes inside a source file, if present." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "1619cbc6", + "metadata": {}, + "outputs": [], + "source": [ + "sp.dataframe.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccee8900", + "metadata": {}, + "outputs": [], + "source": [ + "sp.add_energy_offset(constant=16.2)" + ] + }, { "cell_type": "code", "execution_count": null, From f7977a6c07e230a488e679fe3fd4603b30c04063 Mon Sep 17 00:00:00 2001 From: rettigl Date: Fri, 14 Jun 2024 22:06:24 +0200 Subject: [PATCH 108/300] add bias from file to energy calibration --- sed/calibrator/energy.py | 14 +++++-- sed/config/mpes_example_config.yaml | 8 ++-- sed/core/processor.py | 24 ++++++++++-- sed/loader/mpes/loader.py | 38 +++++++++++++++++-- ...for_example_time-resolved_ARPES_data.ipynb | 15 +------- .../6_binning_with_time-stamped_data.ipynb | 5 ++- 6 files changed, 74 insertions(+), 30 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index cf37d998..c5092f7d 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -1532,6 +1532,9 @@ def add_offsets( offsets["creation_date"] = datetime.now().timestamp() # column-based offsets if columns is not None: + if isinstance(columns, str): + columns = [columns] + if weights is None: weights = 1 if isinstance(weights, (int, float, np.integer, np.floating)): @@ -1543,10 +1546,13 @@ def add_offsets( if not all(isinstance(s, (int, float, np.integer, np.floating)) for s in weights): raise TypeError(f"Invalid type for weights: {type(weights)}") - if isinstance(columns, str): - columns = [columns] - if isinstance(preserve_mean, bool): - preserve_mean = [preserve_mean] * len(columns) + if preserve_mean is None: + preserve_mean = False + if not isinstance(preserve_mean, Sequence): + preserve_mean = [preserve_mean] + if len(preserve_mean) == 1: + preserve_mean = [preserve_mean[0]] * len(columns) + if not isinstance(reductions, Sequence): reductions = [reductions] if len(reductions) == 1: diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 4e8a2898..c0d54198 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -33,6 +33,8 @@ dataframe: tof_column: "t" # dataframe column containing analog-to-digital data adc_column: "ADC" + # dataframe column containing bias voltage data + bias_column: "sampleBias" # dataframe column containing corrected x coordinates corrected_x_column: "Xm" # dataframe column containing corrected y coordinates @@ -86,9 +88,9 @@ dataframe: ADC: format: per_electron group_name: "/Stream_4" -# bias_voltage: -# format: per_file -# group_name: "KTOF:Lens:Sample:V" + sampleBias: + format: per_file + group_name: "KTOF:Lens:Sample:V" energy: # Number of bins to use for energy calibration traces diff --git a/sed/core/processor.py b/sed/core/processor.py index d1bd6a6a..e2563157 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1413,6 +1413,7 @@ def save_energy_calibration( def append_energy_axis( self, calibration: dict = None, + bias_voltage: float = None, preview: bool = False, verbose: bool = None, **kwds, @@ -1426,6 +1427,9 @@ def append_energy_axis( calibration (dict, optional): Calibration dict containing calibration parameters. Overrides calibration from class or config. Defaults to None. + bias_voltage (float, optional): Sample bias voltage of the scan data. If omitted, + the bias voltage is being read from the dataframe. If it is not found there, + a warning is printed and the calibrated data will not be offset correctly. preview (bool): Option to preview the first elements of the data frame. verbose (bool, optional): Option to print out diagnostic information. Defaults to config["core"]["verbose"]. @@ -1466,11 +1470,23 @@ def append_energy_axis( else: raise ValueError("No dataframe loaded!") - if preview: - print(self._dataframe.head(10)) + + if bias_voltage is not None: + self.add_energy_offset(constant=bias_voltage, verbose=verbose, preview=preview) + elif self.config["dataframe"]["bias_column"] in self._dataframe.columns: + self.add_energy_offset( + columns=[self.config["dataframe"]["bias_column"]], + verbose=verbose, + preview=preview, + ) else: - if verbose: - print(self._dataframe) + print("Sample bias data not found or provided. Calibrated energy will be offset.") + # Preview only if no offset applied + if preview: + print(self._dataframe.head(10)) + else: + if verbose: + print(self._dataframe) def add_energy_offset( self, diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 153d9f1a..c2b5d797 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -65,8 +65,23 @@ def hdf5_to_dataframe( seach_pattern="Stream", ) - channel_list = [channel for channel in channels.values()] - column_names = [name for name in channels.keys()] + channel_list = [] + column_names = [] + + for name, channel in channels.items(): + if ( + channel["format"] == "per_electron" + and channel["group_name"] in test_proc + or channel["format"] == "per_file" + and channel["group_name"] in test_proc.attrs + ): + channel_list.append(channel) + column_names.append(name) + else: + print( + f"Entry \"{channel['group_name']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) if time_stamps: column_names.append(time_stamp_alias) @@ -139,8 +154,23 @@ def hdf5_to_timed_dataframe( seach_pattern="Stream", ) - channel_list = [channel for channel in channels.values()] - column_names = [name for name in channels.keys()] + channel_list = [] + column_names = [] + + for name, channel in channels.items(): + if ( + channel["format"] == "per_electron" + and channel["group_name"] in test_proc + or channel["format"] == "per_file" + and channel["group_name"] in test_proc.attrs + ): + channel_list.append(channel) + column_names.append(name) + else: + print( + f"Entry \"{channel['group_name']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) if time_stamps: column_names.append(time_stamp_alias) diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 049ace34..335379cd 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -472,12 +472,11 @@ "source": [ "# use the refid of the bias that the measurement was taken at\n", "# Eref can be used to set the absolute energy (kinetic energy, E-EF) of the feature used for energy calibration (if known)\n", - "Eref=-0.5\n", + "Eref=-1.3\n", "# the lmfit method uses a fit of (d/(t-t0))**2 to determine the energy calibration\n", "# limits and starting values for the fitting parameters can be provided as dictionaries\n", "sp.calibrate_energy_axis(\n", " ref_energy=Eref,\n", - " ref_id=0,\n", " method=\"lmfit\",\n", " energy_scale='kinetic',\n", " d={'value':1.0,'min': .7, 'max':1.2, 'vary':True},\n", @@ -524,7 +523,7 @@ "metadata": {}, "outputs": [], "source": [ - "sp.append_energy_axis()" + "sp.append_energy_axis(bias_voltage=16.8)" ] }, { @@ -547,16 +546,6 @@ "sp.dataframe.head()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "ccee8900", - "metadata": {}, - "outputs": [], - "source": [ - "sp.add_energy_offset(constant=16.2)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index dcde5d6f..28e91e9b 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True)" + "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True, verbose=True)" ] }, { @@ -157,7 +157,7 @@ "source": [ "# Load energy calibration EDCs\n", "scans = np.arange(127,136)\n", - "voltages = np.arange(22,13,-1)\n", + "voltages = np.arange(21,12,-1)\n", "files = [caldir + r'/Scan' + str(num).zfill(4) + '_1.h5' for num in scans]\n", "sp.load_bias_series(data_files=files, normalize=True, biases=voltages, ranges=[(64000, 76000)])\n", "rg = (65500, 66000)\n", @@ -173,6 +173,7 @@ "outputs": [], "source": [ "# Apply stored config energy calibration\n", + "#sp.append_energy_axis(bias_voltage=17)\n", "sp.append_energy_axis()" ] }, From db0ca77f01db51c77cbb6669e3939ab0f7d8031d Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 19 Jun 2024 21:18:04 +0200 Subject: [PATCH 109/300] minor bugfixes --- sed/config/mpes_example_config.yaml | 6 +++++- sed/loader/mpes/loader.py | 2 +- ...sion_pipeline_for_example_time-resolved_ARPES_data.ipynb | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index c0d54198..b2984580 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -75,19 +75,23 @@ dataframe: # dataframe channels and group names to read from the h5 files channels: - # The timestamp + # The X-channel X: format: per_electron group_name: "/Stream_0" + # The Y-channel Y: format: per_electron group_name: "/Stream_1" + # The tof-channel t: format: per_electron group_name: "/Stream_2" + # The ADC-channel ADC: format: per_electron group_name: "/Stream_4" + # The sample Bias-channel sampleBias: format: per_file group_name: "KTOF:Lens:Sample:V" diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c2b5d797..d2106566 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -41,7 +41,7 @@ def hdf5_to_dataframe( Args: files (List[str]): A list of the file paths to load. channels (dict[str, str], optional): hdf5 channels names to load. Each entry in the dict - should contain the keys "format" and "groupName". Defaults to load all groups + should contain the keys "format" and "group_name". Defaults to load all groups containing "Stream", and to read the attribute "Name" from each group. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 335379cd..c5ecb0a9 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -470,7 +470,6 @@ "metadata": {}, "outputs": [], "source": [ - "# use the refid of the bias that the measurement was taken at\n", "# Eref can be used to set the absolute energy (kinetic energy, E-EF) of the feature used for energy calibration (if known)\n", "Eref=-1.3\n", "# the lmfit method uses a fit of (d/(t-t0))**2 to determine the energy calibration\n", @@ -669,7 +668,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, From d240ed35525506162afd3c6efce469a67dd9a9ff Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 19 Jun 2024 21:19:13 +0200 Subject: [PATCH 110/300] rename group_name to dataset_key --- sed/config/mpes_example_config.yaml | 14 ++-- sed/loader/mpes/loader.py | 122 ++++++++++++++-------------- tests/data/loader/mpes/config.yaml | 82 +++++++++++++++++++ 3 files changed, 150 insertions(+), 68 deletions(-) diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index b2984580..13751714 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -18,9 +18,9 @@ dataframe: # dataframe column name for the time stamp column time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) - ms_markers_group: "msMarkers" + ms_markers_key: "/msMarkers" # hdf5 attribute containing the timestamp of the first event in a file - first_event_time_stamp_key: "FirstEventTimeStamp" + first_event_time_stamp_key: "/FirstEventTimeStamp" # Time stepping in seconds of the succesive events in the timed dataframe timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to @@ -78,23 +78,23 @@ dataframe: # The X-channel X: format: per_electron - group_name: "/Stream_0" + dataset_key: "/Stream_0" # The Y-channel Y: format: per_electron - group_name: "/Stream_1" + dataset_key: "/Stream_1" # The tof-channel t: format: per_electron - group_name: "/Stream_2" + dataset_key: "/Stream_2" # The ADC-channel ADC: format: per_electron - group_name: "/Stream_4" + dataset_key: "/Stream_4" # The sample Bias-channel sampleBias: format: per_file - group_name: "KTOF:Lens:Sample:V" + dataset_key: "/KTOF:Lens:Sample:V" energy: # Number of bins to use for energy calibration traces diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index d2106566..97a46a42 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -31,7 +31,7 @@ def hdf5_to_dataframe( channels: dict[str, Any] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", **kwds, ) -> ddf.DataFrame: @@ -41,13 +41,13 @@ def hdf5_to_dataframe( Args: files (List[str]): A list of the file paths to load. channels (dict[str, str], optional): hdf5 channels names to load. Each entry in the dict - should contain the keys "format" and "group_name". Defaults to load all groups + should contain the keys "format" and "dataset_key". Defaults to load all groups containing "Stream", and to read the attribute "Name" from each group. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. time_stamp_alias (str): Alias name for the timestamp column. Defaults to "timeStamps". - ms_markers_group (str): h5 column containing timestamp information. + ms_markers_key (str): hdf5 path containing timestamp information. Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". @@ -60,7 +60,7 @@ def hdf5_to_dataframe( test_proc = h5py.File(files[test_fid]) if channels is None: - channels = get_groups_and_aliases( + channels = get_datasets_and_aliases( h5file=test_proc, seach_pattern="Stream", ) @@ -71,15 +71,15 @@ def hdf5_to_dataframe( for name, channel in channels.items(): if ( channel["format"] == "per_electron" - and channel["group_name"] in test_proc + and channel["dataset_key"] in test_proc or channel["format"] == "per_file" - and channel["group_name"] in test_proc.attrs + and channel["dataset_key"] in test_proc.attrs ): channel_list.append(channel) column_names.append(name) else: print( - f"Entry \"{channel['group_name']}\" for channel \"{name}\" not found.", + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", "Skipping the channel.", ) @@ -90,7 +90,7 @@ def hdf5_to_dataframe( h5file=test_proc, channels=channel_list, time_stamps=time_stamps, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, ) @@ -101,7 +101,7 @@ def hdf5_to_dataframe( h5file=h5py.File(f), channels=channel_list, time_stamps=time_stamps, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, ), dtype=test_array.dtype, @@ -119,7 +119,7 @@ def hdf5_to_timed_dataframe( channels: dict[str, Any] = None, time_stamps: bool = False, time_stamp_alias: str = "timeStamps", - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", **kwds, ) -> ddf.DataFrame: @@ -136,7 +136,7 @@ def hdf5_to_timed_dataframe( False. time_stamp_alias (str): Alias name for the timestamp column. Defaults to "timeStamps". - ms_markers_group (str): h5 column containing timestamp information. + ms_markers_key (str): hdf5 dataset containing timestamp information. Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". @@ -149,7 +149,7 @@ def hdf5_to_timed_dataframe( test_proc = h5py.File(files[test_fid]) if channels is None: - channels = get_groups_and_aliases( + channels = get_datasets_and_aliases( h5file=test_proc, seach_pattern="Stream", ) @@ -160,15 +160,15 @@ def hdf5_to_timed_dataframe( for name, channel in channels.items(): if ( channel["format"] == "per_electron" - and channel["group_name"] in test_proc + and channel["dataset_key"] in test_proc or channel["format"] == "per_file" - and channel["group_name"] in test_proc.attrs + and channel["dataset_key"] in test_proc.attrs ): channel_list.append(channel) column_names.append(name) else: print( - f"Entry \"{channel['group_name']}\" for channel \"{name}\" not found.", + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", "Skipping the channel.", ) @@ -179,7 +179,7 @@ def hdf5_to_timed_dataframe( h5file=test_proc, channels=channel_list, time_stamps=time_stamps, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, ) @@ -190,7 +190,7 @@ def hdf5_to_timed_dataframe( h5file=h5py.File(f), channels=channel_list, time_stamps=time_stamps, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, ), dtype=test_array.dtype, @@ -203,12 +203,12 @@ def hdf5_to_timed_dataframe( return ddf.from_dask_array(array_stack, columns=column_names) -def get_groups_and_aliases( +def get_datasets_and_aliases( h5file: h5py.File, seach_pattern: str = None, alias_key: str = "Name", ) -> dict[str, Any]: - """Read groups and aliases from a provided hdf5 file handle + """Read datasets and aliases from a provided hdf5 file handle Args: h5file (h5py.File): @@ -223,21 +223,21 @@ def get_groups_and_aliases( A dict of aliases and groupnames parsed from the file """ # get group names: - group_names = list(h5file) + dataset_names = list(h5file) # Filter the group names if seach_pattern is None: - filtered_group_names = group_names + filtered_dataset_names = dataset_names else: - filtered_group_names = [name for name in group_names if seach_pattern in name] + filtered_dataset_names = [name for name in dataset_names if seach_pattern in name] alias_dict = {} - for name in filtered_group_names: + for name in filtered_dataset_names: alias_dict[name] = get_attribute(h5file[name], alias_key) return { - alias_dict[name]: {"format": "per_electron", "group_name": name} - for name in filtered_group_names + alias_dict[name]: {"format": "per_electron", "dataset_key": name} + for name in filtered_dataset_names } @@ -245,7 +245,7 @@ def hdf5_to_array( h5file: h5py.File, channels: Sequence[Dict[str, Any]], time_stamps=False, - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", ) -> np.ndarray: """Reads the content of the given groups in an hdf5 file, and returns a @@ -258,7 +258,7 @@ def hdf5_to_array( channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. - ms_markers_group (str): h5 column containing timestamp information. + ms_markers_group (str): hdf5 dataset containing timestamp information. Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". @@ -273,7 +273,7 @@ def hdf5_to_array( nelectrons = 0 for channel in channels: if channel["format"] == "per_electron": - nelectrons = len(h5file[channel["group_name"]]) + nelectrons = len(h5file[channel["dataset_key"]]) break if nelectrons == 0: raise ValueError("No 'per_electron' columns defined, or no hits found in file.") @@ -282,20 +282,20 @@ def hdf5_to_array( data_list = [] for channel in channels: if channel["format"] == "per_electron": - g_dataset = np.asarray(h5file[channel["group_name"]]) + g_dataset = np.asarray(h5file[channel["dataset_key"]]) elif channel["format"] == "per_file": - value = float(get_attribute(h5file, channel["group_name"])) + value = float(get_attribute(h5file, channel["dataset_key"])) g_dataset = np.asarray([value] * nelectrons) else: raise ValueError( - f"Invalid 'format':{channel['format']} for channel {channel['group_name']}.", + f"Invalid 'format':{channel['format']} for channel {channel['dataset_key']}.", ) if "data_type" in channel.keys(): g_dataset = g_dataset.astype(channel["data_type"]) else: g_dataset = g_dataset.astype("float32") if len(g_dataset) != nelectrons: - raise ValueError(f"Inconsistent entries found for channel {channel['group_name']}.") + raise ValueError(f"Inconsistent entries found for channel {channel['dataset_key']}.") data_list.append(g_dataset) # calculate time stamps @@ -304,7 +304,7 @@ def hdf5_to_array( time_stamp_data = np.zeros(nelectrons) # the ms marker contains a list of events that occurred at full ms intervals. # It's monotonically increasing, and can contain duplicates - ms_marker = np.asarray(h5file[ms_markers_group]) + ms_marker = np.asarray(h5file[ms_markers_key]) # try to get start timestamp from "FirstEventTimeStamp" attribute try: @@ -346,7 +346,7 @@ def hdf5_to_timed_array( h5file: h5py.File, channels: Sequence[Dict[str, Any]], time_stamps=False, - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", ) -> np.ndarray: """Reads the content of the given groups in an hdf5 file, and returns a @@ -359,7 +359,7 @@ def hdf5_to_timed_array( channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. - ms_markers_group (str): h5 column containing timestamp information. + ms_markers_group (str): hdf5 dataset containing timestamp information. Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". @@ -373,19 +373,19 @@ def hdf5_to_timed_array( # Read out groups: data_list = [] - ms_marker = np.asarray(h5file[ms_markers_group]) + ms_marker = np.asarray(h5file[ms_markers_key]) for channel in channels: timed_dataset = np.zeros_like(ms_marker) if channel["format"] == "per_electron": - g_dataset = np.asarray(h5file[channel["group_name"]]) + g_dataset = np.asarray(h5file[channel["dataset_key"]]) for i, point in enumerate(ms_marker): timed_dataset[i] = g_dataset[int(point) - 1] elif channel["format"] == "per_file": - value = float(get_attribute(h5file, channel["group_name"])) + value = float(get_attribute(h5file, channel["dataset_key"])) timed_dataset[:] = value else: raise ValueError( - f"Invalid 'format':{channel['format']} for channel {channel['group_name']}.", + f"Invalid 'format':{channel['format']} for channel {channel['dataset_key']}.", ) if "data_type" in channel.keys(): timed_dataset = timed_dataset.astype(channel["data_type"]) @@ -442,20 +442,20 @@ def get_attribute(h5group: h5py.Group, attribute: str) -> str: def get_count_rate( h5file: h5py.File, - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", ) -> tuple[np.ndarray, np.ndarray]: """Create count rate in the file from the msMarker column. Args: h5file (h5py.File): The h5file from which to get the count rate. - ms_markers_group (str, optional): The hdf5 group where the millisecond markers + ms_markers_key (str, optional): The hdf5 path where the millisecond markers are stored. Defaults to "msMarkers". Returns: tuple[np.ndarray, np.ndarray]: The count rate in Hz and the seconds into the scan. """ - ms_markers = np.asarray(h5file[ms_markers_group]) + ms_markers = np.asarray(h5file[ms_markers_key]) secs = np.arange(0, len(ms_markers)) / 1000 msmarker_spline = sint.InterpolatedUnivariateSpline(secs, ms_markers, k=1) rate_spline = msmarker_spline.derivative() @@ -466,19 +466,19 @@ def get_count_rate( def get_elapsed_time( h5file: h5py.File, - ms_markers_group: str = "msMarkers", + ms_markers_key: str = "msMarkers", ) -> float: """Return the elapsed time in the file from the msMarkers wave Args: h5file (h5py.File): The h5file from which to get the count rate. - ms_markers_group (str, optional): The hdf5 group where the millisecond markers + ms_markers_key (str, optional): The hdf5 path where the millisecond markers are stored. Defaults to "msMarkers". Return: float: The acquision time of the file in seconds. """ - secs = h5file[ms_markers_group].len() / 1000 + secs = h5file[ms_markers_key].len() / 1000 return secs @@ -572,7 +572,7 @@ def read_dataframe( - **hdf5_groupnames** : List of groupnames to look for in the file. - **hdf5_aliases**: Dictionary of aliases for the groupnames. - **time_stamp_alias**: Alias for the timestamp column - - **ms_markers_group**: Group name of the millisecond marker column. + - **ms_markers_key**: HDF5 path of the millisecond marker column. - **first_event_time_stamp_key**: Attribute name containing the start timestamp of the file. @@ -622,10 +622,10 @@ def read_dataframe( "timeStamps", ), ) - ms_markers_group = kwds.pop( - "ms_markers_group", + ms_markers_key = kwds.pop( + "ms_markers_key", self._config.get("dataframe", {}).get( - "ms_markers_group", + "ms_markers_key", "msMarkers", ), ) @@ -641,7 +641,7 @@ def read_dataframe( channels=channels, time_stamps=time_stamps, time_stamp_alias=time_stamp_alias, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, **kwds, ) @@ -650,7 +650,7 @@ def read_dataframe( channels=channels, time_stamps=time_stamps, time_stamp_alias=time_stamp_alias, - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, **kwds, ) @@ -936,7 +936,7 @@ def get_count_rate( include. Defaults to list of all file ids. kwds: Keyword arguments: - - **ms_markers_group**: Name of the hdf5 group containing the ms-markers + - **ms_markers_key**: HDF5 path of the ms-markers Returns: tuple[np.ndarray, np.ndarray]: Arrays containing countrate and seconds @@ -945,10 +945,10 @@ def get_count_rate( if fids is None: fids = range(0, len(self.files)) - ms_markers_group = kwds.pop( - "ms_markers_group", + ms_markers_key = kwds.pop( + "ms_markers_key", self._config.get("dataframe", {}).get( - "ms_markers_group", + "ms_markers_key", "msMarkers", ), ) @@ -959,7 +959,7 @@ def get_count_rate( for fid in fids: count_rate_, secs_ = get_count_rate( h5py.File(self.files[fid]), - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, ) secs_list.append((accumulated_time + secs_).T) count_rate_list.append(count_rate_.T) @@ -979,7 +979,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float: include. Defaults to list of all file ids. kwds: Keyword arguments: - - **ms_markers_group**: Name of the hdf5 group containing the ms-markers + - **ms_markers_key**: HDF5 path of the millisecond marker column. Return: float: The elapsed time in the files in seconds. @@ -987,10 +987,10 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float: if fids is None: fids = range(0, len(self.files)) - ms_markers_group = kwds.pop( - "ms_markers_group", + ms_markers_key = kwds.pop( + "ms_markers_key", self._config.get("dataframe", {}).get( - "ms_markers_group", + "ms_markers_key", "msMarkers", ), ) @@ -999,7 +999,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float: for fid in fids: secs += get_elapsed_time( h5py.File(self.files[fid]), - ms_markers_group=ms_markers_group, + ms_markers_key=ms_markers_key, ) return secs diff --git a/tests/data/loader/mpes/config.yaml b/tests/data/loader/mpes/config.yaml index 877c531a..a81523b5 100644 --- a/tests/data/loader/mpes/config.yaml +++ b/tests/data/loader/mpes/config.yaml @@ -1,3 +1,85 @@ core: paths: data_raw_dir: "tests/data/loader/mpes/" + +dataframe: + # dataframe column name for the time stamp column + time_stamp_alias: "timeStamps" + # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) + ms_markers_key: "/msMarkers" + # hdf5 attribute containing the timestamp of the first event in a file + first_event_time_stamp_key: "/FirstEventTimeStamp" + # Time stepping in seconds of the succesive events in the timed dataframe + timed_dataframe_unit_time: 0.001 + # list of columns to apply jitter to + jitter_cols: ["X", "Y", "t", "ADC"] + # dataframe column containing x coordinates + x_column: "X" + # dataframe column containing y coordinates + y_column: "Y" + # dataframe column containing time-of-flight data + tof_column: "t" + # dataframe column containing analog-to-digital data + adc_column: "ADC" + # dataframe column containing bias voltage data + bias_column: "sampleBias" + # dataframe column containing corrected x coordinates + corrected_x_column: "Xm" + # dataframe column containing corrected y coordinates + corrected_y_column: "Ym" + # dataframe column containing corrected time-of-flight data + corrected_tof_column: "tm" + # dataframe column containing kx coordinates + kx_column: "kx" + # dataframe column containing ky coordinates + ky_column: "ky" + # dataframe column containing energy data + energy_column: "energy" + # dataframe column containing delay data + delay_column: "delay" + # time length of a base time-of-flight bin in ns + tof_binwidth: 4.125e-12 + # Binning factor of the tof_column-data compared to tof_binwidth (2^(tof_binning-1)) + tof_binning: 2 + # binning factor used for the adc coordinate (2^(adc_binning-1)) + adc_binning: 3 + # Default units for dataframe entries + units: + X: 'step' + Y: 'step' + t: 'step' + tof_voltage: 'V' + extractor_voltage: 'V' + extractor_current: 'A' + cryo_temperature: 'K' + sample_temperature: 'K' + dld_time: 'ns' + delay: 'ps' + timeStamp: 's' + energy: 'eV' + E: 'eV' + kx: '1/A' + ky: '1/A' + + # dataframe channels and group names to read from the h5 files + channels: + # The X-channel + X: + format: per_electron + dataset_key: "/Stream_0" + # The Y-channel + Y: + format: per_electron + dataset_key: "/Stream_1" + # The tof-channel + t: + format: per_electron + dataset_key: "/Stream_2" + # The ADC-channel + ADC: + format: per_electron + dataset_key: "/Stream_4" + # The sample Bias-channel + sampleBias: + format: per_file + dataset_key: "/KTOF:Lens:Sample:V" From 9fea323c526e25afa9c3766f723fc54ff5061fd6 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 19 Jun 2024 22:10:16 +0200 Subject: [PATCH 111/300] fix config --- sed/config/mpes_example_config.yaml | 14 +++++++------- tests/data/loader/mpes/config.yaml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 13751714..6ee9a00d 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -18,9 +18,9 @@ dataframe: # dataframe column name for the time stamp column time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) - ms_markers_key: "/msMarkers" + ms_markers_key: "msMarkers" # hdf5 attribute containing the timestamp of the first event in a file - first_event_time_stamp_key: "/FirstEventTimeStamp" + first_event_time_stamp_key: "FirstEventTimeStamp" # Time stepping in seconds of the succesive events in the timed dataframe timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to @@ -78,23 +78,23 @@ dataframe: # The X-channel X: format: per_electron - dataset_key: "/Stream_0" + dataset_key: "Stream_0" # The Y-channel Y: format: per_electron - dataset_key: "/Stream_1" + dataset_key: "Stream_1" # The tof-channel t: format: per_electron - dataset_key: "/Stream_2" + dataset_key: "Stream_2" # The ADC-channel ADC: format: per_electron - dataset_key: "/Stream_4" + dataset_key: "Stream_4" # The sample Bias-channel sampleBias: format: per_file - dataset_key: "/KTOF:Lens:Sample:V" + dataset_key: "KTOF:Lens:Sample:V" energy: # Number of bins to use for energy calibration traces diff --git a/tests/data/loader/mpes/config.yaml b/tests/data/loader/mpes/config.yaml index a81523b5..1a411be2 100644 --- a/tests/data/loader/mpes/config.yaml +++ b/tests/data/loader/mpes/config.yaml @@ -6,9 +6,9 @@ dataframe: # dataframe column name for the time stamp column time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) - ms_markers_key: "/msMarkers" + ms_markers_key: "msMarkers" # hdf5 attribute containing the timestamp of the first event in a file - first_event_time_stamp_key: "/FirstEventTimeStamp" + first_event_time_stamp_key: "FirstEventTimeStamp" # Time stepping in seconds of the succesive events in the timed dataframe timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to @@ -66,20 +66,20 @@ dataframe: # The X-channel X: format: per_electron - dataset_key: "/Stream_0" + dataset_key: "Stream_0" # The Y-channel Y: format: per_electron - dataset_key: "/Stream_1" + dataset_key: "Stream_1" # The tof-channel t: format: per_electron - dataset_key: "/Stream_2" + dataset_key: "Stream_2" # The ADC-channel ADC: format: per_electron - dataset_key: "/Stream_4" + dataset_key: "Stream_4" # The sample Bias-channel sampleBias: format: per_file - dataset_key: "/KTOF:Lens:Sample:V" + dataset_key: "KTOF:Lens:Sample:V" From 20f0603e06d2e912b3367bb6d9c86c0be9bb211c Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 22 Jun 2024 23:13:58 +0200 Subject: [PATCH 112/300] faster version of per_file channels --- sed/loader/mpes/loader.py | 144 ++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 97a46a42..31c4e6bf 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -65,30 +65,26 @@ def hdf5_to_dataframe( seach_pattern="Stream", ) - channel_list = [] + electron_channels = [] column_names = [] for name, channel in channels.items(): - if ( - channel["format"] == "per_electron" - and channel["dataset_key"] in test_proc - or channel["format"] == "per_file" - and channel["dataset_key"] in test_proc.attrs - ): - channel_list.append(channel) - column_names.append(name) - else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", - "Skipping the channel.", - ) + if channel["format"] == "per_electron": + if channel["dataset_key"] in test_proc: + electron_channels.append(channel) + column_names.append(name) + else: + print( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) if time_stamps: column_names.append(time_stamp_alias) test_array = hdf5_to_array( h5file=test_proc, - channels=channel_list, + channels=electron_channels, time_stamps=time_stamps, ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, @@ -99,7 +95,7 @@ def hdf5_to_dataframe( da.from_delayed( dask.delayed(hdf5_to_array)( h5file=h5py.File(f), - channels=channel_list, + channels=electron_channels, time_stamps=time_stamps, ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, @@ -111,7 +107,25 @@ def hdf5_to_dataframe( ] array_stack = da.concatenate(arrays, axis=1).T - return ddf.from_dask_array(array_stack, columns=column_names) + dataframe = ddf.from_dask_array(array_stack, columns=column_names) + + for name, channel in channels.items(): + if channel["format"] == "per_file": + if channel["dataset_key"] in test_proc.attrs: + values = [float(get_attribute(h5py.File(f), channel["dataset_key"])) for f in files] + delayeds = [ + add_value(partition, name, value) + for partition, value in zip(dataframe.partitions, values) + ] + dataframe = ddf.from_delayed(delayeds) + + else: + print( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) + + return dataframe def hdf5_to_timed_dataframe( @@ -154,30 +168,26 @@ def hdf5_to_timed_dataframe( seach_pattern="Stream", ) - channel_list = [] + electron_channels = [] column_names = [] for name, channel in channels.items(): - if ( - channel["format"] == "per_electron" - and channel["dataset_key"] in test_proc - or channel["format"] == "per_file" - and channel["dataset_key"] in test_proc.attrs - ): - channel_list.append(channel) - column_names.append(name) - else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", - "Skipping the channel.", - ) + if channel["format"] == "per_electron": + if channel["dataset_key"] in test_proc: + electron_channels.append(channel) + column_names.append(name) + else: + print( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) if time_stamps: column_names.append(time_stamp_alias) test_array = hdf5_to_timed_array( h5file=test_proc, - channels=channel_list, + channels=electron_channels, time_stamps=time_stamps, ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, @@ -188,7 +198,7 @@ def hdf5_to_timed_dataframe( da.from_delayed( dask.delayed(hdf5_to_timed_array)( h5file=h5py.File(f), - channels=channel_list, + channels=electron_channels, time_stamps=time_stamps, ms_markers_key=ms_markers_key, first_event_time_stamp_key=first_event_time_stamp_key, @@ -200,7 +210,41 @@ def hdf5_to_timed_dataframe( ] array_stack = da.concatenate(arrays, axis=1).T - return ddf.from_dask_array(array_stack, columns=column_names) + dataframe = ddf.from_dask_array(array_stack, columns=column_names) + + for name, channel in channels.items(): + if channel["format"] == "per_file": + if channel["dataset_key"] in test_proc.attrs: + values = [float(get_attribute(h5py.File(f), channel["dataset_key"])) for f in files] + delayeds = [ + add_value(partition, name, value) + for partition, value in zip(dataframe.partitions, values) + ] + dataframe = ddf.from_delayed(delayeds) + + else: + print( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + "Skipping the channel.", + ) + + return dataframe + + +@dask.delayed +def add_value(partition: ddf.DataFrame, name: str, value: float) -> ddf.DataFrame: + """Dask delayed helper function to add a value to each dataframe partition + + Args: + partition (ddf.DataFrame): Dask dataframe partition + name (str): Name of the column to add + value (float): value to add to this partition + + Returns: + ddf.DataFrame: Dataframe partition with added column + """ + partition[name] = value + return partition def get_datasets_and_aliases( @@ -254,7 +298,7 @@ def hdf5_to_array( Args: h5file (h5py.File): hdf5 file handle to read from - electron_channels (Sequence[Dict[str, any]]): + channels (Sequence[Dict[str, any]]): channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. @@ -268,40 +312,25 @@ def hdf5_to_array( """ # Delayed array for loading an HDF5 file of reasonable size (e.g. < 1GB) - - # determine group length from per_electron column: - nelectrons = 0 - for channel in channels: - if channel["format"] == "per_electron": - nelectrons = len(h5file[channel["dataset_key"]]) - break - if nelectrons == 0: - raise ValueError("No 'per_electron' columns defined, or no hits found in file.") - # Read out groups: data_list = [] for channel in channels: if channel["format"] == "per_electron": g_dataset = np.asarray(h5file[channel["dataset_key"]]) - elif channel["format"] == "per_file": - value = float(get_attribute(h5file, channel["dataset_key"])) - g_dataset = np.asarray([value] * nelectrons) else: raise ValueError( f"Invalid 'format':{channel['format']} for channel {channel['dataset_key']}.", ) - if "data_type" in channel.keys(): - g_dataset = g_dataset.astype(channel["data_type"]) + if "dtype" in channel.keys(): + g_dataset = g_dataset.astype(channel["dtype"]) else: g_dataset = g_dataset.astype("float32") - if len(g_dataset) != nelectrons: - raise ValueError(f"Inconsistent entries found for channel {channel['dataset_key']}.") data_list.append(g_dataset) # calculate time stamps if time_stamps: # create target array for time stamps - time_stamp_data = np.zeros(nelectrons) + time_stamp_data = np.zeros(len(data_list[0])) # the ms marker contains a list of events that occurred at full ms intervals. # It's monotonically increasing, and can contain duplicates ms_marker = np.asarray(h5file[ms_markers_key]) @@ -355,7 +384,7 @@ def hdf5_to_timed_array( Args: h5file (h5py.File): hdf5 file handle to read from - electron_channels (Sequence[Dict[str, any]]): + channels (Sequence[Dict[str, any]]): channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. @@ -380,15 +409,12 @@ def hdf5_to_timed_array( g_dataset = np.asarray(h5file[channel["dataset_key"]]) for i, point in enumerate(ms_marker): timed_dataset[i] = g_dataset[int(point) - 1] - elif channel["format"] == "per_file": - value = float(get_attribute(h5file, channel["dataset_key"])) - timed_dataset[:] = value else: raise ValueError( f"Invalid 'format':{channel['format']} for channel {channel['dataset_key']}.", ) - if "data_type" in channel.keys(): - timed_dataset = timed_dataset.astype(channel["data_type"]) + if "dtype" in channel.keys(): + timed_dataset = timed_dataset.astype(channel["dtype"]) else: timed_dataset = timed_dataset.astype("float32") From 34684b14f0aaa965211357df6c29ec537a1ffe49 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 00:56:58 +0200 Subject: [PATCH 113/300] remove map_partitions from offset_column function (using direct assignment) --- sed/core/dfops.py | 51 +++++++++-------------------------------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/sed/core/dfops.py b/sed/core/dfops.py index b1f8779c..0058ae44 100644 --- a/sed/core/dfops.py +++ b/sed/core/dfops.py @@ -392,53 +392,20 @@ def offset_by_other_columns( "Please open a request on GitHub if this feature is required.", ) - # calculate the mean of the columns to reduce - means = { - col: dask.delayed(df[col].mean()) - for col, red, pm in zip(offset_columns, reductions, preserve_mean) - if red or pm - } - - # define the functions to apply the offsets - def shift_by_mean(x, cols, signs, means, flip_signs=False): - """Shift the target column by the mean of the offset columns.""" - for col in cols: - s = -signs[col] if flip_signs else signs[col] - x[target_column] = x[target_column] + s * means[col] - return x[target_column] - - def shift_by_row(x, cols, signs): - """Apply the offsets to the target column.""" - for col in cols: - x[target_column] = x[target_column] + signs[col] * x[col] - return x[target_column] - # apply offset from the reduced columns - df[target_column] = df.map_partitions( - shift_by_mean, - cols=[col for col, red in zip(offset_columns, reductions) if red], - signs=signs_dict, - means=means, - meta=df[target_column].dtype, - ) + for col, red in zip(offset_columns, reductions): + if red == "mean": + df[target_column] = df[target_column] + signs_dict[col] * df[col].mean() # apply offset from the offset columns - df[target_column] = df.map_partitions( - shift_by_row, - cols=[col for col, red in zip(offset_columns, reductions) if not red], - signs=signs_dict, - meta=df[target_column].dtype, - ) + for col, red in zip(offset_columns, reductions): + if not red: + df[target_column] = df[target_column] + signs_dict[col] * df[col] # compensate shift from the preserved mean columns if any(preserve_mean): - df[target_column] = df.map_partitions( - shift_by_mean, - cols=[col for col, pmean in zip(offset_columns, preserve_mean) if pmean], - signs=signs_dict, - means=means, - flip_signs=True, - meta=df[target_column].dtype, - ) + for col, pmean in zip(offset_columns, preserve_mean): + if pmean: + df[target_column] = df[target_column] - signs_dict[col] * df[col].mean() return df From 39d83ae42edec5c4501c987259e52905674ab9e4 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 00:57:49 +0200 Subject: [PATCH 114/300] enable energy offset in benchmark --- benchmarks/benchmark_sed.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index 3b633c30..ba181f42 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -121,6 +121,7 @@ def test_workflow_1d() -> None: system_config={}, verbose=True, ) + processor.dataframe["sampleBias"] = 16.7 processor.add_jitter() processor.apply_momentum_correction() processor.apply_momentum_calibration() @@ -155,6 +156,7 @@ def test_workflow_4d() -> None: system_config={}, verbose=True, ) + processor.dataframe["sampleBias"] = 16.7 processor.add_jitter() processor.apply_momentum_correction() processor.apply_momentum_calibration() From ac1aa18fe6dad31ebba5310b4bdec8caed420d08 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 23 Jun 2024 21:29:18 +0200 Subject: [PATCH 115/300] typing fixes --- sed/loader/mpes/loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 31c4e6bf..616467bf 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -9,8 +9,8 @@ import glob import json import os -from typing import Any from collections.abc import Sequence +from typing import Any from urllib.error import HTTPError from urllib.error import URLError from urllib.request import urlopen @@ -287,7 +287,7 @@ def get_datasets_and_aliases( def hdf5_to_array( h5file: h5py.File, - channels: Sequence[Dict[str, Any]], + channels: Sequence[dict[str, Any]], time_stamps=False, ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", @@ -298,7 +298,7 @@ def hdf5_to_array( Args: h5file (h5py.File): hdf5 file handle to read from - channels (Sequence[Dict[str, any]]): + channels (Sequence[dict[str, any]]): channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. @@ -373,7 +373,7 @@ def hdf5_to_array( def hdf5_to_timed_array( h5file: h5py.File, - channels: Sequence[Dict[str, Any]], + channels: Sequence[dict[str, Any]], time_stamps=False, ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", @@ -384,7 +384,7 @@ def hdf5_to_timed_array( Args: h5file (h5py.File): hdf5 file handle to read from - channels (Sequence[Dict[str, any]]): + channels (Sequence[dict[str, any]]): channel dicts containing group names and types to read. time_stamps (bool, optional): Option to calculate time stamps. Defaults to False. From ec92432b8549bfaa99a6b1b296d5dd3750c0aeda Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 25 Jun 2024 10:07:28 +0200 Subject: [PATCH 116/300] apply bias offset for energy calibration in energy calibrator, and omitt from offset dict to avoid applying twice. --- sed/calibrator/energy.py | 23 ++++++++++++++---- sed/core/processor.py | 24 ++++++------------- ...adata_collection_and_export_to_NeXus.ipynb | 4 ++-- tutorial/4_hextof_workflow.ipynb | 11 ++++----- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index c5092f7d..4c436175 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -779,6 +779,7 @@ def append_energy_axis( tof_column: str = None, energy_column: str = None, calibration: dict = None, + bias_voltage: float = None, verbose: bool = True, **kwds, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: @@ -794,6 +795,9 @@ def append_energy_axis( calibration (dict, optional): Calibration dictionary. If provided, overrides calibration from class or config. Defaults to self.calibration or config["energy"]["calibration"]. + bias_voltage (float, optional): Sample bias voltage of the scan data. If omitted, + the bias voltage is being read from the dataframe. If it is not found there, + a warning is printed and the calibrated data might have an offset. verbose (bool, optional): Option to print out diagnostic information. Defaults to True. **kwds: additional keyword arguments for the energy conversion. They are @@ -882,6 +886,20 @@ def append_energy_axis( else: raise NotImplementedError + # apply bias offset + scale_sign: Literal[-1, 1] = -1 if calibration["energy_scale"] == "binding" else 1 + if bias_voltage is not None: + df[energy_column] = df[energy_column] + scale_sign * bias_voltage + elif self._config["dataframe"]["bias_column"] in df.columns: + df = dfops.offset_by_other_columns( + df=df, + target_column=energy_column, + offset_columns=self._config["dataframe"]["bias_column"], + weights=scale_sign, + ) + else: + print("Sample bias data not found or provided. Calibrated energy might be incorrect.") + metadata = self.gather_calibration_metadata(calibration) return df, metadata @@ -1636,10 +1654,7 @@ def add_offsets( if constant: if not isinstance(constant, (int, float, np.integer, np.floating)): raise TypeError(f"Invalid type for constant: {type(constant)}") - df[energy_column] = df.map_partitions( - lambda x: x[energy_column] + constant, - meta=(energy_column, np.float64), - ) + df[energy_column] = df[energy_column] + constant self.offsets = offsets metadata["offsets"] = offsets diff --git a/sed/core/processor.py b/sed/core/processor.py index e2563157..194c05e2 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1429,7 +1429,7 @@ def append_energy_axis( Defaults to None. bias_voltage (float, optional): Sample bias voltage of the scan data. If omitted, the bias voltage is being read from the dataframe. If it is not found there, - a warning is printed and the calibrated data will not be offset correctly. + a warning is printed and the calibrated data might have an offset. preview (bool): Option to preview the first elements of the data frame. verbose (bool, optional): Option to print out diagnostic information. Defaults to config["core"]["verbose"]. @@ -1447,6 +1447,7 @@ def append_energy_axis( df, metadata = self.ec.append_energy_axis( df=self._dataframe, calibration=calibration, + bias_voltage=bias_voltage, verbose=verbose, **kwds, ) @@ -1454,6 +1455,7 @@ def append_energy_axis( tdf, _ = self.ec.append_energy_axis( df=self._timed_dataframe, calibration=calibration, + bias_voltage=bias_voltage, verbose=False, **kwds, ) @@ -1470,23 +1472,11 @@ def append_energy_axis( else: raise ValueError("No dataframe loaded!") - - if bias_voltage is not None: - self.add_energy_offset(constant=bias_voltage, verbose=verbose, preview=preview) - elif self.config["dataframe"]["bias_column"] in self._dataframe.columns: - self.add_energy_offset( - columns=[self.config["dataframe"]["bias_column"]], - verbose=verbose, - preview=preview, - ) + if preview: + print(self._dataframe.head(10)) else: - print("Sample bias data not found or provided. Calibrated energy will be offset.") - # Preview only if no offset applied - if preview: - print(self._dataframe.head(10)) - else: - if verbose: - print(self._dataframe) + if verbose: + print(self._dataframe) def add_energy_offset( self, diff --git a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb index 4de5cf2b..6e9d05ec 100644 --- a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb +++ b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb @@ -219,7 +219,7 @@ "outputs": [], "source": [ "# Apply stored config energy calibration\n", - "sp.append_energy_axis()" + "sp.append_energy_axis(bias_voltage=16.8)" ] }, { @@ -307,7 +307,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.9.19" } }, "nbformat": 4, diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 5cb79585..96a4c2eb 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -585,7 +585,7 @@ "metadata": {}, "source": [ "### correct offsets\n", - "The energy axis is now correct, but still the curves do not stack on eachother as we are not compensating for the `sampleBias`. In the same way, we can compensate the photon energy (`monocrhomatorPhotonEnergy`) and the `tofVoltage` " + "The energy axis is now correct, taking the sample bias of the measurement into account. Additionally, we can compensate the photon energy (`monocrhomatorPhotonEnergy`) and the `tofVoltage`." ] }, { @@ -595,10 +595,9 @@ "outputs": [], "source": [ "sp.add_energy_offset(\n", - " constant=-32, # Sample bias used as reference for energy calibration\n", - " columns=['sampleBias','monochromatorPhotonEnergy','tofVoltage'],\n", - " weights=[1,-1,-1],\n", - " preserve_mean=[False, True, True],\n", + " columns=['monochromatorPhotonEnergy','tofVoltage'],\n", + " weights=[-1,-1],\n", + " preserve_mean=[True, True],\n", ")" ] }, @@ -982,7 +981,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.9.19" } }, "nbformat": 4, From 7d60c9242d0151c84567e3af9307fb7aaced099b Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 25 Jun 2024 10:18:50 +0200 Subject: [PATCH 117/300] fix test --- sed/calibrator/energy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 4c436175..1c4c614e 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -847,6 +847,8 @@ def append_energy_axis( elif "coeffs" in calibration and "E0" in calibration: calibration["calib_type"] = "poly" + if "energy_scale" not in calibration: + calibration["energy_scale"] = "kinetic" else: raise ValueError("No valid calibration parameters provided!") From ff134a557d059bafdeccd72cdf3e66f9a9291fd7 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 25 Jun 2024 21:17:44 +0200 Subject: [PATCH 118/300] fix spelling in all files using Code Spell Checker --- docs/misc/contributing.rst | 2 +- docs/misc/maintain.rst | 2 +- docs/sed/config.rst | 8 ++-- docs/sed/dataset.rst | 2 +- sed/binning/binning.py | 14 +++--- sed/binning/numba_bin.py | 8 ++-- sed/binning/utils.py | 4 +- sed/calibrator/delay.py | 4 +- sed/calibrator/energy.py | 14 +++--- sed/calibrator/momentum.py | 47 +++++++++---------- sed/config/NXmpes_config.json | 2 +- sed/config/default.yaml | 16 +++---- sed/config/flash_example_config.yaml | 6 +-- sed/config/mpes_example_config.yaml | 24 +++++----- sed/core/config.py | 4 +- sed/core/dfops.py | 4 +- sed/core/metadata.py | 2 +- sed/core/processor.py | 32 ++++++------- sed/io/hdf5.py | 4 +- sed/io/nexus.py | 6 +-- sed/loader/base/loader.py | 2 +- sed/loader/flash/loader.py | 4 +- sed/loader/generic/loader.py | 4 +- sed/loader/mirrorutil.py | 8 ++-- sed/loader/mpes/loader.py | 22 ++++----- sed/loader/sxp/loader.py | 6 +-- tests/calibrator/test_energy.py | 6 +-- tests/calibrator/test_momentum.py | 32 ++++++------- tests/helpers.py | 2 +- tests/loader/test_mirrorutil.py | 2 +- tests/test_binning.py | 2 +- tests/test_processor.py | 8 ++-- tutorial/1_binning_fake_data.ipynb | 4 +- ...for_example_time-resolved_ARPES_data.ipynb | 18 +++---- ...adata_collection_and_export_to_NeXus.ipynb | 4 +- tutorial/4_hextof_workflow.ipynb | 10 ++-- .../6_binning_with_time-stamped_data.ipynb | 8 ++-- .../7_correcting_orthorhombic_symmetry.ipynb | 6 +-- tutorial/8_jittering_tutorial.ipynb | 14 +++--- 39 files changed, 183 insertions(+), 184 deletions(-) diff --git a/docs/misc/contributing.rst b/docs/misc/contributing.rst index 21d1c484..ab12e517 100644 --- a/docs/misc/contributing.rst +++ b/docs/misc/contributing.rst @@ -73,7 +73,7 @@ Development Workflow 3. **Write Tests:** If your contribution introduces new features or fixes a bug, add tests to cover your changes. -4. **Run Tests:** To ensure no funtionality is broken, run the tests: +4. **Run Tests:** To ensure no functionality is broken, run the tests: .. code-block:: bash diff --git a/docs/misc/maintain.rst b/docs/misc/maintain.rst index 6e887502..486f426a 100644 --- a/docs/misc/maintain.rst +++ b/docs/misc/maintain.rst @@ -140,7 +140,7 @@ To create a release, follow these steps: c. **If you don't see update on PyPI:** - Visit the GitHub Actions page and monitor the Release workflow (https://github.com/OpenCOMPES/sed/actions/workflows/release.yml). - - Check if errors occured. + - Check if errors occurred. **Understanding the Release Workflow** diff --git a/docs/sed/config.rst b/docs/sed/config.rst index c5457fe0..4cf9d293 100644 --- a/docs/sed/config.rst +++ b/docs/sed/config.rst @@ -1,11 +1,11 @@ Config =================================================== -The config module contains a mechanis to collect configuration parameters from various sources and configuration files, and to combine them in a hierachical manner into a single, consistent configuration dictionary. +The config module contains a mechanics to collect configuration parameters from various sources and configuration files, and to combine them in a hierarchical manner into a single, consistent configuration dictionary. It will load an (optional) provided config file, or alternatively use a passed python dictionary as initial config dictionary, and subsequently look for the following additional config files to load: * ``folder_config``: A config file of name :file:`sed_config.yaml` in the current working directory. This is mostly intended to pass calibration parameters of the workflow between different notebook instances. -* ``user_config``: A config file provided by the user, stored as :file:`.sed/config.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings. -* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/sed/config.yaml` on Linux-based systems, and :file:`%ALLUSERPROFILE%/sed/config.yaml` on Windows. This should provide all necessary default parameters for using the sed processor with a given setup. For an example for an mpes setup, see :ref:`example_config` +* ``user_config``: A config file provided by the user, stored as :file:`.config/sed/config.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings. +* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/sed/config.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/sed/config.yaml` on Windows. This should provide all necessary default parameters for using the sed processor with a given setup. For an example for an mpes setup, see :ref:`example_config` * ``default_config``: The default configuration shipped with the package. Typically, all parameters here should be overwritten by any of the other configuration files. The config mechanism returns the combined dictionary, and reports the loaded configuration files. In order to disable or overwrite any of the configuration files, they can be also given as optional parameters (path to a file, or python dictionary). @@ -13,7 +13,7 @@ The config mechanism returns the combined dictionary, and reports the loaded con API *************************************************** -.. automodule:: sed.core.config +.. automobile:: sed.core.config :members: :undoc-members: diff --git a/docs/sed/dataset.rst b/docs/sed/dataset.rst index 6a8c57f8..05bd94c0 100644 --- a/docs/sed/dataset.rst +++ b/docs/sed/dataset.rst @@ -64,7 +64,7 @@ Setting the “use_existing” keyword to False allows to download the data in a Interrupting extraction has similar behavior to download and just continues from where it stopped. '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' -Or if user deletes the extracted documents, it reextracts from zip file +Or if user deletes the extracted documents, it re-extracts from zip file ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' .. code:: python diff --git a/sed/binning/binning.py b/sed/binning/binning.py index 69a5c44b..4f904275 100644 --- a/sed/binning/binning.py +++ b/sed/binning/binning.py @@ -45,7 +45,7 @@ def bin_partition( - an integer describing the number of bins for all dimensions. This requires "ranges" to be defined as well. - A sequence containing one entry of the following types for each - dimenstion: + dimension: - an integer describing the number of bins. This requires "ranges" to be defined as well. @@ -76,14 +76,14 @@ def bin_partition( jittering. To specify the jitter amplitude or method (normal or uniform noise) a dictionary can be passed. This should look like jitter={'axis':{'amplitude':0.5,'mode':'uniform'}}. - This example also shows the default behaviour, in case None is + This example also shows the default behavior, in case None is passed in the dictionary, or jitter is a list of strings. Warning: this is not the most performing approach. Applying jitter on the dataframe before calling the binning is much faster. Defaults to None. return_edges (bool, optional): If True, returns a list of D arrays describing the bin edges for each dimension, similar to the - behaviour of ``np.histogramdd``. Defaults to False. + behavior of ``np.histogramdd``. Defaults to False. skip_test (bool, optional): Turns off input check and data transformation. Defaults to False as it is intended for internal use only. Warning: setting this True might make error tracking difficult. @@ -127,7 +127,7 @@ def bin_partition( else: bins = cast(list[int], bins) # shift ranges by half a bin size to align the bin centers to the given ranges, - # as the histogram functions interprete the ranges as limits for the edges. + # as the histogram functions interpret the ranges as limits for the edges. for i, nbins in enumerate(bins): halfbinsize = (ranges[i][1] - ranges[i][0]) / (nbins) / 2 ranges[i] = ( @@ -221,7 +221,7 @@ def bin_dataframe( - an integer describing the number of bins for all dimensions. This requires "ranges" to be defined as well. - A sequence containing one entry of the following types for each - dimenstion: + dimension: - an integer describing the number of bins. This requires "ranges" to be defined as well. @@ -260,7 +260,7 @@ def bin_dataframe( jittering. To specify the jitter amplitude or method (normal or uniform noise) a dictionary can be passed. This should look like jitter={'axis':{'amplitude':0.5,'mode':'uniform'}}. - This example also shows the default behaviour, in case None is + This example also shows the default behavior, in case None is passed in the dictionary, or jitter is a list of strings. Warning: this is not the most performing approach. applying jitter on the dataframe before calling the binning is much faster. @@ -466,7 +466,7 @@ def normalization_histogram_from_timed_dataframe( bin_centers: np.ndarray, time_unit: float, ) -> xr.DataArray: - """Get a normalization histogram from a timed datafram. + """Get a normalization histogram from a timed dataframe. Args: df (dask.dataframe.DataFrame): a dask.DataFrame on which to perform the diff --git a/sed/binning/numba_bin.py b/sed/binning/numba_bin.py index 24f79c57..7daeda97 100644 --- a/sed/binning/numba_bin.py +++ b/sed/binning/numba_bin.py @@ -22,7 +22,7 @@ def _hist_from_bin_range( bit integers. Args: - sample (np.ndarray): The data to be histogrammed with shape N,D. + sample (np.ndarray): The data to be histogram'd with shape N,D. bins (Sequence[int]): The number of bins for each dimension D. ranges (np.ndarray): A sequence of length D, each an optional (lower, upper) tuple giving the outer bin edges to be used if the edges are @@ -47,7 +47,7 @@ def _hist_from_bin_range( for i in range(ndims): delta[i] = 1 / ((ranges[i, 1] - ranges[i, 0]) / bins[i]) - strides[i] = hist.strides[i] // hist.itemsize # pylint: disable=E1136 + strides[i] = hist.strides[i] // hist.itemsize for t in range(sample.shape[0]): is_inside = True @@ -155,7 +155,7 @@ def numba_histogramdd( bins: int | Sequence[int] | Sequence[np.ndarray] | np.ndarray, ranges: Sequence = None, ) -> tuple[np.ndarray, list[np.ndarray]]: - """Multidimensional histogramming function, powered by Numba. + """Multidimensional histogram function, powered by Numba. Behaves in total much like numpy.histogramdd. Returns uint32 arrays. This was chosen because it has a significant performance improvement over @@ -165,7 +165,7 @@ def numba_histogramdd( sizes. Args: - sample (np.ndarray): The data to be histogrammed with shape N,D + sample (np.ndarray): The data to be histogram'd with shape N,D bins (int | Sequence[int] | Sequence[np.ndarray] | np.ndarray): The number of bins for each dimension D, or a sequence of bin edges on which to calculate the histogram. diff --git a/sed/binning/utils.py b/sed/binning/utils.py index 6d0acc7a..d5b387ad 100644 --- a/sed/binning/utils.py +++ b/sed/binning/utils.py @@ -31,7 +31,7 @@ def simplify_binning_arguments( - an integer describing the number of bins for all dimensions. This requires "ranges" to be defined as well. - A sequence containing one entry of the following types for each - dimenstion: + dimension: - an integer describing the number of bins. This requires "ranges" to be defined as well. @@ -115,7 +115,7 @@ def simplify_binning_arguments( f"Ranges must be a sequence, not {type(ranges)}.", ) - # otherwise, all bins should by np.ndarrays here + # otherwise, all bins should be of type np.ndarray here elif all(isinstance(x, np.ndarray) for x in bins): bins = cast(list[np.ndarray], list(bins)) else: diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index e6d40af6..78dd1e3b 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -101,7 +101,7 @@ def append_delay_axis( Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: dataframe with added column - and delay calibration metdata dictionary. + and delay calibration metadata dictionary. """ # pylint: disable=duplicate-code if calibration is None: @@ -406,7 +406,7 @@ def mm_to_ps( delay_mm: float | np.ndarray, time0_mm: float, ) -> float | np.ndarray: - """Converts a delaystage position in mm into a relative delay in picoseconds + """Converts a delay stage position in mm into a relative delay in picoseconds (double pass). Args: diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 07ec39bc..b3cbefc8 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -444,7 +444,7 @@ def add_ranges( traces (np.ndarray, optional): Collection of energy dispersion curves. Defaults to self.traces_normed. infer_others (bool, optional): Option to infer the feature detection range - in other traces from a given one using a time warp algorthm. + in other traces from a given one using a time warp algorithm. Defaults to True. mode (str, optional): Specification on how to change the feature ranges ('append' or 'replace'). Defaults to "replace". @@ -1155,7 +1155,7 @@ def common_apply_func(apply: bool): # noqa: ARG001 update(correction["amplitude"], x_center, y_center, diameter=correction["diameter"]) except KeyError as exc: raise ValueError( - "Parameter 'diameter' required for correction type 'sperical', ", + "Parameter 'diameter' required for correction type 'spherical', ", "but not present!", ) from exc @@ -1337,7 +1337,7 @@ def apply_energy_correction( Defaults to config["energy"]["correction_type"]. amplitude (float, optional): Amplitude of the time-of-flight correction term. Defaults to config["energy"]["correction"]["correction_type"]. - correction (dict, optional): Correction dictionary containing paramters + correction (dict, optional): Correction dictionary containing parameters for the correction. Defaults to self.correction or config["energy"]["correction"]. verbose (bool, optional): Option to print out diagnostic information. @@ -1938,7 +1938,7 @@ def _datacheck_peakdetect( x_axis: np.ndarray, y_axis: np.ndarray, ) -> tuple[np.ndarray, np.ndarray]: - """Input format checking for 1D peakdtect algorithm + """Input format checking for 1D peakdetect algorithm Args: x_axis (np.ndarray): x-axis array @@ -2108,7 +2108,7 @@ def fit_energy_calibration( binwidth (float): Time width of each original TOF bin in ns. binning (int): Binning factor of the TOF values. ref_id (int, optional): Reference dataset index. Defaults to 0. - ref_energy (float, optional): Energy value of the feature in the refence + ref_energy (float, optional): Energy value of the feature in the reference trace (eV). required to output the calibration. Defaults to None. t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. @@ -2130,7 +2130,7 @@ def fit_energy_calibration( Returns: dict: A dictionary of fitting parameters including the following, - - "coeffs": Fitted function coefficents. + - "coeffs": Fitted function coefficients. - "axis": Fitted energy axis. """ vals = np.asarray(vals) @@ -2247,7 +2247,7 @@ def poly_energy_calibration( each EDC. order (int, optional): Polynomial order of the fitting function. Defaults to 3. ref_id (int, optional): Reference dataset index. Defaults to 0. - ref_energy (float, optional): Energy value of the feature in the refence + ref_energy (float, optional): Energy value of the feature in the reference trace (eV). required to output the calibration. Defaults to None. t (list[float] | np.ndarray, optional): Array of TOF values. Required to calculate calibration trace. Defaults to None. diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 7c009136..7cb16bb3 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -333,7 +333,7 @@ def add_features( Option to calculate symmetry scores. Defaults to False. **kwds: Keyword arguments. - - **symtype** (str): Type of symmetry scores to calculte + - **symtype** (str): Type of symmetry scores to calculate if symscores is True. Defaults to "rotation". Raises: @@ -781,7 +781,7 @@ def spline_warp_estimate( self.slice_corrected = corrected_image if verbose: - print("Calulated thin spline correction based on the following landmarks:") + print("Calculated thin spline correction based on the following landmarks:") print(f"pouter: {self.pouter}") if use_center: print(f"pcent: {self.pcent}") @@ -883,7 +883,7 @@ def coordinate_transform( - rotation_auto. - scaling. - scaling_auto. - - homomorphy. + - homography. keep (bool, optional): Option to keep the specified coordinate transform in the class. Defaults to False. @@ -1003,7 +1003,7 @@ def coordinate_transform( ) self.slice_transformed = slice_transformed else: - # if external image is provided, apply only the new addional tranformation + # if external image is provided, apply only the new additional transformation slice_transformed = ndi.map_coordinates( image, [rdeform, cdeform], @@ -1036,7 +1036,7 @@ def pose_adjustment( Args: transformations (dict, optional): Dictionary with transformations. - Defaults to self.transformations or config["momentum"]["transformtions"]. + Defaults to self.transformations or config["momentum"]["transformations"]. apply (bool, optional): Option to directly apply the provided transformations. Defaults to False. @@ -1413,11 +1413,10 @@ def select_k_range( equiscale: bool = True, apply: bool = False, ): - """Interactive selection function for features for the Momentum axes calibra- - tion. It allows the user to select the pixel positions of two symmetry points - (a and b) and the k-space distance of the two. Alternatively, the corrdinates - of both points can be provided. See the equiscale option for details on the - specifications of point coordinates. + """Interactive selection function for features for the Momentum axes calibration. It allows + the user to select the pixel positions of two symmetry points (a and b) and the k-space + distance of the two. Alternatively, the coordinates of both points can be provided. See the + equiscale option for details on the specifications of point coordinates. Args: point_a (np.ndarray | list[int], optional): Pixel coordinates of the @@ -1686,7 +1685,7 @@ def apply_corrections( Args: df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply - the distotion correction to. + the distortion correction to. x_column (str, optional): Label of the 'X' column before momentum distortion correction. Defaults to config["momentum"]["x_column"]. y_column (str, optional): Label of the 'Y' column before momentum @@ -1777,11 +1776,11 @@ def gather_correction_metadata(self) -> dict: metadata["registration"]["creation_date"] = datetime.now().timestamp() metadata["registration"]["applied"] = True metadata["registration"]["depends_on"] = ( - "/entry/process/registration/tranformations/rot_z" + "/entry/process/registration/transformations/rot_z" if "angle" in metadata["registration"] and metadata["registration"]["angle"] - else "/entry/process/registration/tranformations/trans_y" + else "/entry/process/registration/transformations/trans_y" if "xtrans" in metadata["registration"] and metadata["registration"]["xtrans"] - else "/entry/process/registration/tranformations/trans_x" + else "/entry/process/registration/transformations/trans_x" if "ytrans" in metadata["registration"] and metadata["registration"]["ytrans"] else "." ) @@ -1805,7 +1804,7 @@ def gather_correction_metadata(self) -> dict: [0.0, 1.0, 0.0], ) metadata["registration"]["trans_y"]["depends_on"] = ( - "/entry/process/registration/tranformations/trans_x" + "/entry/process/registration/transformations/trans_x" if "ytrans" in metadata["registration"] and metadata["registration"]["ytrans"] else "." ) @@ -1821,9 +1820,9 @@ def gather_correction_metadata(self) -> dict: (metadata["registration"]["center"], [0.0]), ) metadata["registration"]["rot_z"]["depends_on"] = ( - "/entry/process/registration/tranformations/trans_y" + "/entry/process/registration/transformations/trans_y" if "xtrans" in metadata["registration"] and metadata["registration"]["xtrans"] - else "/entry/process/registration/tranformations/trans_x" + else "/entry/process/registration/transformations/trans_x" if "ytrans" in metadata["registration"] and metadata["registration"]["ytrans"] else "." ) @@ -1844,7 +1843,7 @@ def append_k_axis( Args: df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the - distotion correction to. + distortion correction to. x_column (str, optional): Label of the source 'X' column. Defaults to config["momentum"]["corrected_x_column"] or config["momentum"]["x_column"] (whichever is present). @@ -1891,7 +1890,7 @@ def append_k_axis( calibration["creation_date"] = datetime.now().timestamp() try: - (df[new_x_column], df[new_y_column]) = detector_coordiantes_2_k_koordinates( + (df[new_x_column], df[new_y_column]) = detector_coordinates_2_k_coordinates( r_det=df[x_column], c_det=df[y_column], r_start=calibration["rstart"], @@ -1947,7 +1946,7 @@ def cm2palette(cmap_name: str) -> list: cmap_name (str): Name of the colormap/palette. Returns: - list: List of colors in hex representation (a bokoeh palette). + list: List of colors in hex representation (a bokeh palette). """ if cmap_name in bp.all_palettes.keys(): palette_func = getattr(bp, cmap_name) @@ -1986,7 +1985,7 @@ def dictmerge( return main_dict -def detector_coordiantes_2_k_koordinates( +def detector_coordinates_2_k_coordinates( r_det: float, c_det: float, r_start: float, @@ -1998,7 +1997,7 @@ def detector_coordiantes_2_k_koordinates( r_step: float, c_step: float, ) -> tuple[float, float]: - """Conversion from detector coordinates (rdet, cdet) to momentum coordinates + """Conversion from detector coordinates (r_det, c_det) to momentum coordinates (kr, kc). Args: @@ -2037,7 +2036,7 @@ def apply_dfield( Args: df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the - distotion correction to. + distortion correction to. dfield (np.ndarray): The distortion correction field. 3D matrix, with column and row distortion fields stacked along the first dimension. x_column (str): Label of the 'X' source column. @@ -2069,7 +2068,7 @@ def generate_inverse_dfield( bin_ranges: list[tuple], detector_ranges: list[tuple], ) -> np.ndarray: - """Generate inverse deformation field using inperpolation with griddata. + """Generate inverse deformation field using interpolation with griddata. Assuming the binning range of the input ``rdeform_field`` and ``cdeform_field`` covers the whole detector. diff --git a/sed/config/NXmpes_config.json b/sed/config/NXmpes_config.json index 94861027..a636ef62 100755 --- a/sed/config/NXmpes_config.json +++ b/sed/config/NXmpes_config.json @@ -353,7 +353,7 @@ }, "/ENTRY[entry]/PROCESS[process]/REGISTRATION[registration]": { "depends_on": "@attrs:metadata/momentum_correction/registration/depends_on", - "TRANSFORMATIONS[tranformations]": { + "TRANSFORMATIONS[transformations]": { "AXISNAME[trans_x]": "@attrs:metadata/momentum_correction/registration/trans_x/value", "AXISNAME[trans_x]/@transformation_type": "@attrs:metadata/momentum_correction/registration/trans_x/type", "AXISNAME[trans_x]/@units": "@attrs:metadata/momentum_correction/registration/trans_x/units", diff --git a/sed/config/default.yaml b/sed/config/default.yaml index b75b42d9..d0f779be 100644 --- a/sed/config/default.yaml +++ b/sed/config/default.yaml @@ -37,9 +37,9 @@ dataframe: adc_binning: 1 # list of columns to apply jitter to. jitter_cols: ["@x_column", "@y_column", "@tof_column"] - # Jitter amplitude or list of jitter amplitudes. Should equal half the digitial step size of each jitter_column + # Jitter amplitude or list of jitter amplitudes. Should equal half the digital step size of each jitter_column jitter_amps: 0.5 - # Time stepping in seconds of the succesive events in the timed dataframe + # Time stepping in seconds of the successive events in the timed dataframe timed_dataframe_unit_time: 0.001 energy: @@ -57,7 +57,7 @@ energy: fastdtw_radius: 2 # Window around a peak to make sure that no other peaks are present peak_window: 7 - # Mehtod to use for energy calibration + # Method to use for energy calibration calibration_method: "lmfit" # Energy scale to use for energy calibration energy_scale: "kinetic" @@ -66,11 +66,11 @@ energy: tof_fermi: 132250 # TOF range to visualize for the correction tool around tof_fermi tof_width: [-600, 1000] - # x-intergration range for the correction tool around the center pixel + # x-integration range for the correction tool around the center pixel x_width: [-20, 20] - # y-intergration range for the correction tool around the center pixel + # y-integration range for the correction tool around the center pixel y_width: [-20, 20] - # High intensity cutoff for the visulaization tool + # High intensity cutoff for the visualization tool color_clip: 300 @@ -105,7 +105,7 @@ delay: binning: # Histogram computation mode to use. hist_mode: "numba" - # Mode for hostogram recombination to use + # Mode for histogram recombination to use mode: fast # Whether to display a progress bar pbar: True @@ -117,7 +117,7 @@ binning: histogram: # number of bins used for histogram visualization bins: [80, 80, 80] - # default axes to use for histgram visualization. + # default axes to use for histogram visualization. # Axes names starting with "@" refer to keys in the "dataframe" section axes: ["@x_column", "@y_column", "@tof_column"] # default ranges to use for histogram visualization (in unbinned detector coordinates) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index bbc6940c..6bb05521 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -5,7 +5,7 @@ core: loader: flash # the beamline where experiment took place beamline: pg2 - # the ID number of the beamtimme + # the ID number of the beamtime beamtime_id: 11013410 # the year of the beamtime year: 2023 @@ -89,7 +89,7 @@ dataframe: # channelAlias: # format: per_pulse/per_electron/per_train # group_name: the hdf5 group path - # slice: if the group contains multidim data, where to slice + # slice: if the group contains multidimensional data, where to slice channels: # The timestamp @@ -124,7 +124,7 @@ dataframe: slice: 3 # The auxillary channel has a special structure where the group further contains - # a multidim structure so further aliases are defined below + # a multidimensional structure so further aliases are defined below dldAux: format: per_pulse group_name: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/" diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 95f93218..37a2b5ae 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -7,7 +7,7 @@ core: copy_tool_source: "/path/to/data/" # path to the root or the local data storage copy_tool_dest: "/path/to/localDataStore/" - # optional keyworkds for the copy tool: + # optional keywords for the copy tool: copy_tool_kwds: # number of parallel copy jobs ntasks: 20 @@ -25,11 +25,11 @@ dataframe: Stream_4: "ADC" # dataframe column name for the time stamp column time_stamp_alias: "timeStamps" - # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) + # hdf5 group name containing eventIDs occurring at every millisecond (used to calculate timestamps) ms_markers_group: "msMarkers" # hdf5 attribute containing the timestamp of the first event in a file first_event_time_stamp_key: "FirstEventTimeStamp" - # Time stepping in seconds of the succesive events in the timed dataframe + # Time stepping in seconds of the successive events in the timed dataframe timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to jitter_cols: ["X", "Y", "t", "ADC"] @@ -96,7 +96,7 @@ energy: fastdtw_radius: 2 # Window around a peak to make sure that no other peaks are present peak_window: 7 - # Mehtod to use for energy calibration + # Method to use for energy calibration calibration_method: "lmfit" # Energy scale to use for energy calibration energy_scale: "kinetic" @@ -105,11 +105,11 @@ energy: tof_fermi: 132250 # TOF range to visualize for the correction tool around tof_fermi tof_width: [-600, 1000] - # x-intergration range for the correction tool around the center pixel + # x-integration range for the correction tool around the center pixel x_width: [-20, 20] - # y-intergration range for the correction tool around the center pixel + # y-integration range for the correction tool around the center pixel y_width: [-20, 20] - # High intensity cutoff for the visulaization tool + # High intensity cutoff for the visualization tool color_clip: 300 correction: # Correction type @@ -155,9 +155,9 @@ momentum: sigma_radius: 1 # default momentum calibration calibration: - # x momentum scaleing factor + # x momentum scaling factor kx_scale: 0.010729535670610963 - # y momentum scaleing factor + # y momentum scaling factor ky_scale: 0.010729535670610963 # x BZ center pixel x_center: 256.0 @@ -195,7 +195,7 @@ delay: binning: # Histogram computation mode to use. hist_mode: "numba" - # Mode for hostogram recombination to use + # Mode for histogram recombination to use mode: "fast" # Whether to display a progress bar pbar: True @@ -209,7 +209,7 @@ binning: histogram: # number of bins used for histogram visualization bins: [80, 80, 80, 80] - # default axes to use for histgram visualization. + # default axes to use for histogram visualization. # Axes names starting with "@" refer to keys in the "dataframe" section axes: ["@x_column", "@y_column", "@tof_column", "@adc_column"] # default ranges to use for histogram visualization (in unbinned detector coordinates) @@ -307,6 +307,6 @@ nexus: reader: "mpes" # NeXus application definition to use for saving definition: "NXmpes" - # List conatining additional input files to be handed to the pynxtools converter tool, + # List containing additional input files to be handed to the pynxtools converter tool, # e.g. containing a configuration file, and additional metadata. input_files: ["../sed/config/NXmpes_config.json"] diff --git a/sed/core/config.py b/sed/core/config.py index 868d2f06..f359c426 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -173,10 +173,10 @@ def load_config(config_path: str) -> dict: def save_config(config_dict: dict, config_path: str, overwrite: bool = False): """Function to save a given config dictionary to a json or yaml file. Normally, it loads any existing file of the given name, and keeps any existing dictionary keys not present in the - provided dictionary. The overwrite option creates a fully empty dictionry first. + provided dictionary. The overwrite option creates a fully empty dictionary first. Args: - config_dict (dict): The dictionry to save. + config_dict (dict): The dictionary to save. config_path (str): A string containing the path to the file where to save the dictionary to. overwrite (bool, optional): Option to overwrite an existing file with the given dictionary. diff --git a/sed/core/dfops.py b/sed/core/dfops.py index b1f8779c..f449e481 100644 --- a/sed/core/dfops.py +++ b/sed/core/dfops.py @@ -31,7 +31,7 @@ def apply_jitter( with added jitter. Defaults to None. amps (float | Sequence[float], optional): Amplitude scalings for the jittering noise. If one number is given, the same is used for all axes. - For normal noise, the added noise will have sdev [-amp, +amp], for + For normal noise, the added noise will have stdev [-amp, +amp], for uniform noise it will cover the interval [-amp, +amp]. Defaults to 0.5. jitter_type (str, optional): the type of jitter to add. 'uniform' or 'normal' @@ -205,7 +205,7 @@ def forward_fill_lazy( Allows forward filling between partitions. This is useful for dataframes that have sparse data, such as those with many NaNs. - Runnin the forward filling multiple times can fix the issue of having + Running the forward filling multiple times can fix the issue of having entire partitions consisting of NaNs. By default we run this twice, which is enough to fix the issue for dataframes with no consecutive partitions of NaNs. diff --git a/sed/core/metadata.py b/sed/core/metadata.py index 77531987..d38fe313 100644 --- a/sed/core/metadata.py +++ b/sed/core/metadata.py @@ -101,7 +101,7 @@ def add( Args: entry: dictionary containing the metadata to add. name: name of the dictionary key under which to add entry. - duplicate_policy: Control behaviour in case the 'name' key + duplicate_policy: Control behavior in case the 'name' key is already present in the metadata dictionary. Can be any of: - "raise": raises a DuplicateEntryError. diff --git a/sed/core/processor.py b/sed/core/processor.py index f02ac65c..b334c1a8 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -329,7 +329,7 @@ def normalization_histogram(self) -> xr.DataArray: """Getter attribute for the normalization histogram Returns: - xr.DataArray: The normalizazion histogram + xr.DataArray: The normalization histogram """ if self._normalization_histogram is None: raise ValueError("No normalization histogram available, generate histogram first!") @@ -540,7 +540,7 @@ def define_features( ): """2. Step of the distortion correction workflow: Define feature points in momentum space. They can be either manually selected using a GUI tool, be - ptovided as list of feature points, or auto-generated using a + provided as list of feature points, or auto-generated using a feature-detection algorithm. Args: @@ -589,7 +589,7 @@ def generate_splinewarp( **kwds, ): """3. Step of the distortion correction workflow: Generate the correction - function restoring the symmetry in the image using a splinewarp algortihm. + function restoring the symmetry in the image using a splinewarp algorithm. Args: use_center (bool, optional): Option to use the position of the @@ -686,7 +686,7 @@ def pose_adjustment( Args: transformations (dict[str, Any], optional): Dictionary with transformations. - Defaults to self.transformations or config["momentum"]["transformtions"]. + Defaults to self.transformations or config["momentum"]["transformations"]. apply (bool, optional): Option to directly apply the provided transformations. Defaults to False. use_correction (bool, option): Whether to use the spline warp correction @@ -705,7 +705,7 @@ def pose_adjustment( if verbose is None: verbose = self.verbose - # Generate homomorphy as default if no distortion correction has been applied + # Generate homography as default if no distortion correction has been applied if self.mc.slice_corrected is None: if self.mc.slice is None: self.mc.slice = np.zeros(self._config["momentum"]["bins"][0:2]) @@ -852,7 +852,7 @@ def calibrate_momentum_axes( second point used for momentum calibration. Defaults to config["momentum"]["center_pixel"]. k_distance (float, optional): Momentum distance between point a and b. - Needs to be provided if no specific k-koordinates for the two points + Needs to be provided if no specific k-coordinates for the two points are given. Defaults to None. k_coord_a (np.ndarray | list[float], optional): Momentum coordinate of the first point used for calibration. Used if equiscale is False. @@ -989,7 +989,7 @@ def adjust_energy_correction( apply=False, **kwds, ): - """1. step of the energy crrection workflow: Opens an interactive plot to + """1. step of the energy correction workflow: Opens an interactive plot to adjust the parameters for the TOF/energy correction. Also pre-bins the data if they are not present yet. @@ -1068,7 +1068,7 @@ def apply_energy_correction( verbose: bool = None, **kwds, ): - """2. step of the energy correction workflow: Apply the enery correction + """2. step of the energy correction workflow: Apply the energy correction parameters stored in the class to the dataframe. Args: @@ -1243,7 +1243,7 @@ def find_bias_peaks( radius (int, optional): Radius parameter for fast_dtw. Defaults to config["energy"]["fastdtw_radius"]. peak_window (int, optional): Peak_window parameter for the peak detection - algorthm. amount of points that have to have to behave monotoneously + algorithm. amount of points that have to have to behave monotonously around a peak. Defaults to config["energy"]["peak_window"]. apply (bool, optional): Option to directly apply the provided parameters. Defaults to False. @@ -1960,7 +1960,7 @@ def add_jitter( """Add jitter to the selected dataframe columns. Args: - cols (list[str], optional): The colums onto which to apply jitter. + cols (list[str], optional): The columns onto which to apply jitter. Defaults to config["dataframe"]["jitter_cols"]. amps (float | Sequence[float], optional): Amplitude scalings for the jittering noise. If one number is given, the same is used for all axes. @@ -2148,7 +2148,7 @@ def compute( ranges (Sequence[tuple[float, float]], optional): list of tuples containing the start and end point of the binning range. Defaults to None. normalize_to_acquisition_time (bool | str): Option to normalize the - result to the acquistion time. If a "slow" axis was scanned, providing + result to the acquisition time. If a "slow" axis was scanned, providing the name of the scanned axis will compute and apply the corresponding normalization histogram. Defaults to False. **kwds: Keyword arguments: @@ -2375,12 +2375,12 @@ def view_event_histogram( Args: dfpid (int): Number of the data frame partition to look at. ncol (int, optional): Number of columns in the plot grid. Defaults to 2. - bins (Sequence[int], optional): Number of bins to use for the speicified + bins (Sequence[int], optional): Number of bins to use for the specified axes. Defaults to config["histogram"]["bins"]. axes (Sequence[str], optional): Names of the axes to display. Defaults to config["histogram"]["axes"]. ranges (Sequence[tuple[float, float]], optional): Value ranges of all - specified axes. Defaults toconfig["histogram"]["ranges"]. + specified axes. Defaults to config["histogram"]["ranges"]. backend (str, optional): Backend of the plotting library ('matplotlib' or 'bokeh'). Defaults to "bokeh". legend (bool, optional): Option to include a legend in the histogram plots. @@ -2462,7 +2462,7 @@ def save( - "*.h5", "*.hdf5": Saves an HDF5 file. - "*.nxs", "*.nexus": Saves a NeXus file. - **kwds: Keyword argumens, which are passed to the writer functions: + **kwds: Keyword arguments, which are passed to the writer functions: For TIFF writing: - **alias_dict**: Dictionary of dimension aliases to use. @@ -2473,9 +2473,9 @@ def save( For NeXus: - - **reader**: Name of the nexustools reader to use. + - **reader**: Name of the pynxtools reader to use. Defaults to config["nexus"]["reader"] - - **definiton**: NeXus application definition to use for saving. + - **definition**: NeXus application definition to use for saving. Must be supported by the used ``reader``. Defaults to config["nexus"]["definition"] - **input_files**: A list of input files to pass to the reader. diff --git a/sed/io/hdf5.py b/sed/io/hdf5.py index 9365c61d..1b6ab35a 100644 --- a/sed/io/hdf5.py +++ b/sed/io/hdf5.py @@ -48,7 +48,7 @@ def recursive_write_metadata(h5group: h5py.Group, node: dict): print(f"Saved {key} as string.") except BaseException as exc: raise ValueError( - f"Unknown error occured, cannot save {item} of type {type(item)}.", + f"Unknown error occurred, cannot save {item} of type {type(item)}.", ) from exc @@ -141,7 +141,7 @@ def load_h5(faddr: str, mode: str = "r") -> xr.DataArray: ValueError: Raised if data or axes are not found in the file. Returns: - xr.DataArray: output xarra data + xr.DataArray: output xarray data """ with h5py.File(faddr, mode) as h5_file: # Reading data array diff --git a/sed/io/nexus.py b/sed/io/nexus.py index c0b10185..2041647c 100644 --- a/sed/io/nexus.py +++ b/sed/io/nexus.py @@ -1,5 +1,5 @@ """This module contains NuXus file input/output functions for the sed.io module. -The conversion is based on the nexusutils from the FAIRmat NFDI consortium. +The conversion is based on the pynxtools from the FAIRmat NFDI consortium. For details, see https://github.com/nomad-coe/nomad-parser-nexus """ @@ -27,9 +27,9 @@ def to_nexus( data._attrs["metadata"]. faddr (str): The file path to save to. reader (str): The name of the NeXus reader to use. - definition (str): The NeXus definiton to use. + definition (str): The NeXus definition to use. input_files (str | Sequence[str]): The file path or paths to the additional files to use. - **kwds: Keyword arguments for ``nexusutils.dataconverter.convert``. + **kwds: Keyword arguments for ``pynxtools.dataconverter.convert.convert()``. """ if isinstance(input_files, str): diff --git a/sed/loader/base/loader.py b/sed/loader/base/loader.py index 61bab069..2b4c4c20 100644 --- a/sed/loader/base/loader.py +++ b/sed/loader/base/loader.py @@ -65,7 +65,7 @@ def read_dataframe( files will be ignored. Defaults to None. runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes - precendence over ``files`` and ``folders``. Defaults to None. + precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File type to read ('parquet', 'json', 'csv', etc). If a folder path is given, all files with the specified extension are read into the dataframe in the reading order. Defaults to None. diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index dfb82088..6402f0cc 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -5,7 +5,7 @@ The dataframe is a amalgamation of all h5 files for a combination of runs, where the NaNs are automatically forward filled across different files. This can then be saved as a parquet for out-of-sed processing and reread back to access other -sed funtionality. +sed functionality. """ from __future__ import annotations @@ -883,7 +883,7 @@ def read_dataframe( Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will - be located in the location provided by ``folders``. Takes precendence over + be located in the location provided by ``folders``. Takes precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. diff --git a/sed/loader/generic/loader.py b/sed/loader/generic/loader.py index e5149968..0444af0f 100644 --- a/sed/loader/generic/loader.py +++ b/sed/loader/generic/loader.py @@ -46,7 +46,7 @@ def read_dataframe( files will be ignored. Defaults to None. runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes - precendence over ``files`` and ``folders``. Defaults to None. + precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File type to read ('parquet', 'json', 'csv', etc). If a folder path is given, all files with the specified extension are read into the dataframe in the reading order. Defaults to "parquet". @@ -59,7 +59,7 @@ def read_dataframe( Raises: ValueError: Raised if neither files nor folder provided. - FileNotFoundError: Raised if the fileds or folder cannot be found. + FileNotFoundError: Raised if the files or folder cannot be found. ValueError: Raised if the file type is not supported. Returns: diff --git a/sed/loader/mirrorutil.py b/sed/loader/mirrorutil.py index e443b329..d6db66e3 100644 --- a/sed/loader/mirrorutil.py +++ b/sed/loader/mirrorutil.py @@ -1,7 +1,7 @@ """ module sed.loader.mirrorutil, code for transparently mirroring file system trees to a second (local) location. This is speeds up binning of data stored on network drives -tremendiously. +tremendously. Mostly ported from https://github.com/mpes-kit/mpes. @author: L. Rettig """ @@ -20,7 +20,7 @@ class CopyTool: """File collecting and sorting class. Args: - source (str): Dource path for the copy tool. + source (str): Source path for the copy tool. dest (str): Destination path for the copy tool. """ @@ -263,7 +263,7 @@ def cleanup_oldest_scan( proceed = input() if proceed == "y": shutil.rmtree(oldest_scan) - print("Removed sucessfully!") + print("Removed successfully!") else: print("Aborted.") @@ -292,7 +292,7 @@ def get_target_dir( ValueError: Raised if sdir not inside of source Returns: - str: The mapped targed directory inside dest + str: The mapped target directory inside dest """ if not os.path.isdir(sdir): diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c28b32f3..5c8a793f 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -69,7 +69,7 @@ def hdf5_to_dataframe( if group_names == []: group_names, alias_dict = get_groups_and_aliases( h5file=test_proc, - seach_pattern="Stream", + search_pattern="Stream", ) column_names = [alias_dict.get(group, group) for group in group_names] @@ -150,7 +150,7 @@ def hdf5_to_timed_dataframe( if group_names == []: group_names, alias_dict = get_groups_and_aliases( h5file=test_proc, - seach_pattern="Stream", + search_pattern="Stream", ) column_names = [alias_dict.get(group, group) for group in group_names] @@ -188,7 +188,7 @@ def hdf5_to_timed_dataframe( def get_groups_and_aliases( h5file: h5py.File, - seach_pattern: str = None, + search_pattern: str = None, alias_key: str = "Name", ) -> tuple[list[str], dict[str, str]]: """Read groups and aliases from a provided hdf5 file handle @@ -196,7 +196,7 @@ def get_groups_and_aliases( Args: h5file (h5py.File): The hdf5 file handle - seach_pattern (str, optional): + search_pattern (str, optional): Search pattern to select groups. Defaults to include all groups. alias_key (str, optional): Attribute key where aliases are stored. Defaults to "Name". @@ -209,10 +209,10 @@ def get_groups_and_aliases( group_names = list(h5file) # Filter the group names - if seach_pattern is None: + if search_pattern is None: filtered_group_names = group_names else: - filtered_group_names = [name for name in group_names if seach_pattern in name] + filtered_group_names = [name for name in group_names if search_pattern in name] alias_dict = {} for name in filtered_group_names: @@ -330,7 +330,7 @@ def hdf5_to_timed_array( timestamp of a file. Defaults to "FirstEventTimeStamp". Returns: - np.ndarray: the array of the values at evently spaced timing obtained from + np.ndarray: the array of the values at evenly spaced timing obtained from the ms_markers. """ @@ -375,7 +375,7 @@ def hdf5_to_timed_array( def get_attribute(h5group: h5py.Group, attribute: str) -> str: - """Reads, decodes and returns an attrubute from an hdf5 group + """Reads, decodes and returns an attribute from an hdf5 group Args: h5group (h5py.Group): @@ -432,7 +432,7 @@ def get_elapsed_time( are stored. Defaults to "msMarkers". Return: - float: The acquision time of the file in seconds. + float: The acquisition time of the file in seconds. """ secs = h5file[ms_markers_group].len() / 1000 @@ -513,7 +513,7 @@ def read_dataframe( files will be ignored. Defaults to None. runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will be located in the location provided by ``folders``. Takes - precendence over ``files`` and ``folders``. Defaults to None. + precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): File extension to use. If a folder path is given, all files with the specified extension are read into the dataframe in the reading order. Defaults to "h5". @@ -832,7 +832,7 @@ def gather_metadata( print("Contrast aperture size not found.") # Storing the lens modes corresponding to lens voltages. - # Use lens volages present in first lens_mode entry. + # Use lens voltages present in first lens_mode entry. lens_list = self._config["metadata"]["lens_mode_config"][ next(iter(self._config["metadata"]["lens_mode_config"])) ].keys() diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index b165257f..96b1f7be 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -6,7 +6,7 @@ The dataframe is a amalgamation of all h5 files for a combination of runs, where the NaNs are automatically forward filled across different files. This can then be saved as a parquet for out-of-sed processing and reread back to access other -sed funtionality. +sed functionality. Most of the structure is identical to the FLASH loader. """ from __future__ import annotations @@ -406,7 +406,7 @@ def create_dataframe_per_electron( """ if self.array_indices is None or len(self.array_indices) != np_array.shape[0]: raise RuntimeError( - "macrobunch_indices not set correctly, internal inconstency detected.", + "macrobunch_indices not set correctly, internal inconsistency detected.", ) train_data = [] for i, _ in enumerate(self.array_indices): @@ -927,7 +927,7 @@ def read_dataframe( Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will - be located in the location provided by ``folders``. Takes precendence over + be located in the location provided by ``folders``. Takes precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 0ea4a062..80c7cbef 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -177,8 +177,8 @@ def test_calibrate_append(energy_scale: str, calibration_method: str) -> None: and the application to the data frame. Args: - energy_scale (str): tpye of energy scaling - calibration_method (str): method used for ralibration + energy_scale (str): type of energy scaling + calibration_method (str): method used for calibration """ config = parse_config( config={"dataframe": {"tof_binning": 2}}, @@ -346,7 +346,7 @@ def test_append_tof_ns_axis() -> None: ) def test_energy_correction(correction_type: str, correction_kwd: dict) -> None: """Function to test if all energy correction functions generate symmetric curves - with the maximum at the cetner x/y. + with the maximum at the center x/y. Args: correction_type (str): type of correction to test diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index 88f050de..437a8082 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -53,7 +53,7 @@ def test_bin_data_and_slice_image() -> None: def test_feature_extract() -> None: - """Testextracting the feature from a 2D slice""" + """Test extracting the feature from a 2D slice""" config = parse_config( config={"core": {"loader": "mpes"}}, folder_config={}, @@ -75,7 +75,7 @@ def test_feature_extract() -> None: [True, False], ) def test_splinewarp(include_center: bool) -> None: - """Test the generation of the splinewarp etimate. + """Test the generation of the splinewarp estimate. Args: include_center (bool): Option to include the center point. @@ -111,7 +111,7 @@ def test_splinewarp(include_center: bool) -> None: def test_ascale() -> None: - """Test the generation of the splinewarp etimate with ascale parameter.""" + """Test the generation of the splinewarp estimate with ascale parameter.""" config = parse_config( config={"core": {"loader": "mpes"}}, folder_config={}, @@ -233,44 +233,44 @@ def test_apply_correction() -> None: ] depends_on_list = [ { - "root": "/entry/process/registration/tranformations/trans_x", + "root": "/entry/process/registration/transformations/trans_x", "axes": {"trans_x": "."}, }, { - "root": "/entry/process/registration/tranformations/trans_y", + "root": "/entry/process/registration/transformations/trans_y", "axes": {"trans_y": "."}, }, { - "root": "/entry/process/registration/tranformations/rot_z", + "root": "/entry/process/registration/transformations/rot_z", "axes": {"rot_z": "."}, }, { - "root": "/entry/process/registration/tranformations/trans_y", + "root": "/entry/process/registration/transformations/trans_y", "axes": { "trans_x": ".", - "trans_y": "/entry/process/registration/tranformations/trans_x", + "trans_y": "/entry/process/registration/transformations/trans_x", }, }, { - "root": "/entry/process/registration/tranformations/rot_z", + "root": "/entry/process/registration/transformations/rot_z", "axes": { "trans_x": ".", - "rot_z": "/entry/process/registration/tranformations/trans_x", + "rot_z": "/entry/process/registration/transformations/trans_x", }, }, { - "root": "/entry/process/registration/tranformations/rot_z", + "root": "/entry/process/registration/transformations/rot_z", "axes": { "trans_y": ".", - "rot_z": "/entry/process/registration/tranformations/trans_y", + "rot_z": "/entry/process/registration/transformations/trans_y", }, }, { - "root": "/entry/process/registration/tranformations/rot_z", + "root": "/entry/process/registration/transformations/rot_z", "axes": { "trans_x": ".", - "trans_y": "/entry/process/registration/tranformations/trans_x", - "rot_z": "/entry/process/registration/tranformations/trans_y", + "trans_y": "/entry/process/registration/transformations/trans_x", + "rot_z": "/entry/process/registration/transformations/trans_y", }, }, ] @@ -323,7 +323,7 @@ def test_apply_registration( mc.add_features(features=features, rotsym=6) mc.spline_warp_estimate() mc.pose_adjustment(**transformations, apply=True) - # disable re-calculation of inverse defield to save time, as we are just testing meta data here + # disable re-calculation of inverse dfield to save time, as we are just testing meta data here mc.dfield_updated = False df, metadata = mc.apply_corrections(df=df) assert "Xm" in df.columns diff --git a/tests/helpers.py b/tests/helpers.py index 2264bee3..007d068d 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -20,7 +20,7 @@ def simulate_binned_data(shape: tuple, dims: list) -> xr.DataArray: """ assert len(dims) == len( shape, - ), "number of dimesions and data shape must coincide" + ), "number of dimensions and data shape must coincide" return xr.DataArray( data=np.random.rand(*shape), diff --git a/tests/loader/test_mirrorutil.py b/tests/loader/test_mirrorutil.py index 468c20fb..d7d522c8 100644 --- a/tests/loader/test_mirrorutil.py +++ b/tests/loader/test_mirrorutil.py @@ -22,7 +22,7 @@ def test_copy_tool_folder() -> None: - """Test the folder copy functionalty of the CopyTool""" + """Test the folder copy functionality of the CopyTool""" dest_folder = tempfile.mkdtemp() gid = os.getgid() ct = CopyTool( diff --git a/tests/test_binning.py b/tests/test_binning.py index dedbec10..74cbd584 100644 --- a/tests/test_binning.py +++ b/tests/test_binning.py @@ -96,7 +96,7 @@ def test_histdd_error_is_raised(_samples: np.ndarray, _bins: list[int]) -> None: ) def test_histdd_bins_as_numpy(args: tuple[np.ndarray, np.ndarray, int]) -> None: """Test whether the numba_histogramdd functions produces the same result - as np.histogramdd if called with a list of bin edgees + as np.histogramdd if called with a list of bin edges Args: args (tuple[np.ndarray, np.ndarray, int]): Tuple of diff --git a/tests/test_processor.py b/tests/test_processor.py index cffb3a5b..1514dd3f 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -131,8 +131,8 @@ def test_processor_from_runs() -> None: def test_additional_parameter_to_loader() -> None: - """Test if additinal keyword parameter can be passed to the loader from the - Processor initialiuzation. + """Test if additional keyword parameter can be passed to the loader from the + Processor initialization. """ config = {"core": {"loader": "generic"}} processor = SedProcessor( @@ -670,7 +670,7 @@ def test_align_dld_sectors() -> None: tof_aligned_array[i][0 : len(val)] = val np.testing.assert_allclose(tof_ref_array, tof_aligned_array + sector_delays[:, np.newaxis]) - # cleanup flash inermediaries + # cleanup flash intermediaries parquet_data_dir = config["core"]["paths"]["data_parquet_dir"] for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) @@ -1005,7 +1005,7 @@ def test_get_normalization_histogram() -> None: metadata["user0"]["name"] = "Name" metadata["user0"]["email"] = "email" metadata["user0"]["affiliation"] = "affiliation" -# NXinstrument +# Instrument metadata["instrument"] = {} # analyzer metadata["instrument"]["analyzer"] = {} diff --git a/tutorial/1_binning_fake_data.ipynb b/tutorial/1_binning_fake_data.ipynb index 61d8873b..04370e8e 100644 --- a/tutorial/1_binning_fake_data.ipynb +++ b/tutorial/1_binning_fake_data.ipynb @@ -9,7 +9,7 @@ "source": [ "# Binning demonstration on locally generated fake data\n", "In this example, we generate a table with random data simulating a single event dataset.\n", - "We showcase the binning method, first on a simple single table using the bin_partition method and then in the distributed mehthod bin_dataframe, using daks dataframes.\n", + "We showcase the binning method, first on a simple single table using the bin_partition method and then in the distributed method bin_dataframe, using daks dataframes.\n", "The first method is never really called directly, as it is simply the function called by the bin_dataframe on each partition of the dask dataframe." ] }, @@ -138,7 +138,7 @@ "source": [ "## Compute distributed binning on the partitioned dask dataframe\n", "In this example, the small dataset does not give significant improvement over the pandas implementation, at least using this number of partitions.\n", - "A single partition would be faster (you can try...) but we use multiple for demonstration purpouses." + "A single partition would be faster (you can try...) but we use multiple for demonstration purposes." ] }, { diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index af695478..fb36f549 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -46,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 Gbyte free space.\n", + "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 GByte free space.\n", "data_path = dataset.dir # This is the path to the data\n", "scandir, caldir = dataset.subdirs # scandir contains the data, caldir contains the calibration files" ] @@ -182,7 +182,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Option whether a central point shall be fixed in the determiantion fo the correction\n", + "# Option whether a central point shall be fixed in the determination fo the correction\n", "sp.generate_splinewarp(include_center=True)" ] }, @@ -213,7 +213,7 @@ "metadata": {}, "source": [ "#### 4. Step:\n", - "To adjust scaling, position and orientation of the corrected momentum space image, you can apply further affine transformations to the distortion correction field. Here, first a postential scaling is applied, next a translation, and finally a rotation around the center of the image (defined via the config). One can either use an interactive tool, or provide the adjusted values and apply them directly." + "To adjust scaling, position and orientation of the corrected momentum space image, you can apply further affine transformations to the distortion correction field. Here, first a potential scaling is applied, next a translation, and finally a rotation around the center of the image (defined via the config). One can either use an interactive tool, or provide the adjusted values and apply them directly." ] }, { @@ -255,9 +255,9 @@ "source": [ "### Momentum calibration workflow\n", "#### 1. Step:\n", - "First, the momentum scaling needs to be calibtrated. Either, one can provide the coordinates of one point outside the center, and provide its distane to the Brillouin zone center (which is assumed to be located in the center of the image), one can specify two points on the image and their distance (where the 2nd point marks the BZ center),or one can provide absolute k-coordinates of two distinct momentum points.\n", + "First, the momentum scaling needs to be calibrated. Either, one can provide the coordinates of one point outside the center, and provide its distane to the Brillouin zone center (which is assumed to be located in the center of the image), one can specify two points on the image and their distance (where the 2nd point marks the BZ center),or one can provide absolute k-coordinates of two distinct momentum points.\n", "\n", - "If no points are provided, an interactive tool is created. Here, left mouse click selectes the off-center point (brillouin_zone_cetnered=True) or toggle-selects the off-center and center point." + "If no points are provided, an interactive tool is created. Here, left mouse click selects the off-center point (brillouin_zone_centered=True) or toggle-selects the off-center and center point." ] }, { @@ -267,7 +267,7 @@ "metadata": {}, "outputs": [], "source": [ - "k_distance = 2/np.sqrt(3)*np.pi/3.28 # k-distance of the K-point in a hexagonal Brilloiun zone\n", + "k_distance = 2/np.sqrt(3)*np.pi/3.28 # k-distance of the K-point in a hexagonal Brillouin zone\n", "#sp.calibrate_momentum_axes(k_distance = k_distance)\n", "point_a = [308, 345]\n", "sp.calibrate_momentum_axes(point_a=point_a, k_distance = k_distance, apply=True)\n", @@ -332,7 +332,7 @@ "metadata": {}, "source": [ "#### 1st step:\n", - "Here, one can select the functional form to be used, and adjust its parameters. The binned data used for the momentum calibration is plotted around the Fermi energy (defined by tof_fermi), and the correction function is plotted ontop. Possible correction functions are: \"sperical\" (parameter: diameter), \"Lorentzian\" (parameter: gamma), \"Gaussian\" (parameter: sigma), and \"Lorentzian_asymmetric\" (parameters: gamma, amplitude2, gamma2).\n", + "Here, one can select the functional form to be used, and adjust its parameters. The binned data used for the momentum calibration is plotted around the Fermi energy (defined by tof_fermi), and the correction function is plotted ontop. Possible correction functions are: \"spherical\" (parameter: diameter), \"Lorentzian\" (parameter: gamma), \"Gaussian\" (parameter: sigma), and \"Lorentzian_asymmetric\" (parameters: gamma, amplitude2, gamma2).\n", "\n", "One can either use an interactive alignment tool, or provide parameters directly." ] @@ -458,7 +458,7 @@ "metadata": {}, "source": [ "#### 3. Step:\n", - "Next, the detected peak positions and bias voltages are used to determine the calibration function. This can be either done by fitting the functional form d^2/(t-t0)^2 via lmfit (\"lmfit\"), or using a polynomial approxiamtion (\"lstsq\" or \"lsqr\"). Here, one can also define a reference id, and a reference energy. Those define the absolute energy position of the feature used for calibration in the \"reference\" trace, at the bias voltage where the final measurement has been performed. The energy scale can be either \"kinetic\" (decreasing energy with increasing TOF), or \"binding\" (increasing energy with increasing TOF).\n", + "Next, the detected peak positions and bias voltages are used to determine the calibration function. This can be either done by fitting the functional form d^2/(t-t0)^2 via lmfit (\"lmfit\"), or using a polynomial approximation (\"lstsq\" or \"lsqr\"). Here, one can also define a reference id, and a reference energy. Those define the absolute energy position of the feature used for calibration in the \"reference\" trace, at the bias voltage where the final measurement has been performed. The energy scale can be either \"kinetic\" (decreasing energy with increasing TOF), or \"binding\" (increasing energy with increasing TOF).\n", "\n", "After calculating the calibration, all traces corrected with the calibration are plotted ontop of each other, the calibration function together with the extracted features is plotted." ] @@ -661,7 +661,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb index 4de5cf2b..42fede36 100644 --- a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb +++ b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb @@ -44,7 +44,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 Gbyte free space.\n", + "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 GByte free space.\n", "data_path = dataset.dir # This is the path to the data\n", "scandir, _ = dataset.subdirs # scandir contains the data, _ contains the calibration files" ] @@ -307,7 +307,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 5cb79585..cfe90f17 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -57,7 +57,7 @@ "outputs": [], "source": [ "# data_path can be defined and used to store the data in a specific location\n", - "dataset.get(\"Gd_W110\") # Put in Path to a storage of at least 10 Gbyte free space.\n", + "dataset.get(\"Gd_W110\") # Put in Path to a storage of at least 10 GByte free space.\n", "path = dataset.dir\n", "print(path)" ] @@ -585,7 +585,7 @@ "metadata": {}, "source": [ "### correct offsets\n", - "The energy axis is now correct, but still the curves do not stack on eachother as we are not compensating for the `sampleBias`. In the same way, we can compensate the photon energy (`monocrhomatorPhotonEnergy`) and the `tofVoltage` " + "The energy axis is now correct, but still the curves do not stack on eachother as we are not compensating for the `sampleBias`. In the same way, we can compensate the photon energy (`monochromatorPhotonEnergy`) and the `tofVoltage` " ] }, { @@ -639,7 +639,7 @@ "metadata": {}, "source": [ "### save the calibration parameters\n", - "The parameters we have found can be saved to a file, so that we can use them later. This means the calibrtion can be used for different runs." + "The parameters we have found can be saved to a file, so that we can use them later. This means the calibration can be used for different runs." ] }, { @@ -826,7 +826,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You may note some intensity variation along the delay axis. This comes mainly from inhomogenous speed of the delay stage, and thus inquivalently time spend on every delay point. This can be corrected for by normalizing the data to the aquisition time per delay point:" + "You may note some intensity variation along the delay axis. This comes mainly from inhomogeneous speed of the delay stage, and thus inequivalent amounts of time spent on every delay point. This can be corrected for by normalizing the data to the acquisition time per delay point:" ] }, { @@ -982,7 +982,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.9.19" } }, "nbformat": 4, diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index dcde5d6f..4d4148a6 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -9,7 +9,7 @@ }, "source": [ "# Binning of temperature-dependent ARPES data using time-stamped external temperature data\n", - "In this example, we pull some temperature-dependent ARPES data from Zenodo, which was recorded as a continous temperture ramp. We then add the respective temperature informtion from the respective timestamp/temperature values to the dataframe, and bin the data as function of temperature\n", + "In this example, we pull some temperature-dependent ARPES data from Zenodo, which was recorded as a continuous temperature ramp. We then add the respective temperature information from the respective timestamp/temperature values to the dataframe, and bin the data as function of temperature\n", "For performance reasons, best store the data on a locally attached storage (no network drive). This can also be achieved transparently using the included MirrorUtil class." ] }, @@ -49,7 +49,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset.get(\"TaS2\") # Put in Path to a storage of at least 20 Gbyte free space.\n", + "dataset.get(\"TaS2\") # Put in Path to a storage of at least 20 GByte free space.\n", "data_path = dataset.dir\n", "scandir, caldir = dataset.subdirs # scandir contains the data, caldir contains the calibration files\n", "\n", @@ -273,7 +273,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Remaining fluctiations are an effect of the varying count rate throught the scan\n", + "# Remaining fluctuations are an effect of the varying count rate throughout the scan\n", "plt.figure()\n", "rate, secs = sp.loader.get_count_rate()\n", "plt.plot(secs, rate)" @@ -342,7 +342,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/tutorial/7_correcting_orthorhombic_symmetry.ipynb b/tutorial/7_correcting_orthorhombic_symmetry.ipynb index 27c6f76a..5d032eff 100644 --- a/tutorial/7_correcting_orthorhombic_symmetry.ipynb +++ b/tutorial/7_correcting_orthorhombic_symmetry.ipynb @@ -47,7 +47,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 Gbyte free space.\n", + "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 GByte free space.\n", "data_path = dataset.dir # This is the path to the data\n", "scandir, _ = dataset.subdirs # scandir contains the data, _ contains the calibration files" ] @@ -112,7 +112,7 @@ "metadata": {}, "source": [ "## Spline-warp generation: \n", - "For the spline-warp generation, we need to tell the algorithm the difference in length of Gamma-K and Gamma-M. This we can do using the ascale parameter, which can either be a single number (the ratio), or a list of length ``rotation_symmetry`` definint the relative length of the respective vectors." + "For the spline-warp generation, we need to tell the algorithm the difference in length of Gamma-K and Gamma-M. This we can do using the ascale parameter, which can either be a single number (the ratio), or a list of length ``rotation_symmetry`` defining the relative length of the respective vectors." ] }, { @@ -234,7 +234,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/tutorial/8_jittering_tutorial.ipynb b/tutorial/8_jittering_tutorial.ipynb index c2788a4b..3aa5583b 100644 --- a/tutorial/8_jittering_tutorial.ipynb +++ b/tutorial/8_jittering_tutorial.ipynb @@ -45,7 +45,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 Gbyte free space.\n", + "dataset.get(\"WSe2\") # Put in Path to a storage of at least 20 GByte free space.\n", "data_path = dataset.dir # This is the path to the data\n", "scandir, _ = dataset.subdirs # scandir contains the data, _ contains the calibration files" ] @@ -107,7 +107,7 @@ "id": "de1d411c", "metadata": {}, "source": [ - "We notice some oscillation ontop of the data. These are re-binning artefacts, originating from a non-integer number of machine-bins per bin, as we can verify by binning with a different number of steps:" + "We notice some oscillation ontop of the data. These are re-binning artifacts, originating from a non-integer number of machine-bins per bin, as we can verify by binning with a different number of steps:" ] }, { @@ -155,9 +155,9 @@ "id": "c2d82124", "metadata": {}, "source": [ - "To mitigate this problem, we can add some randomness to the data, and re-distribute events into the gaps in-between bins. This is also termed `dithering` and e.g. known from image manipulation. The important factor is to add the right amount and right type of random distribution, to end up at a quasi-continous uniform distribution, but not lose information.\n", + "To mitigate this problem, we can add some randomness to the data, and re-distribute events into the gaps in-between bins. This is also termed `dithering` and e.g. known from image manipulation. The important factor is to add the right amount and right type of random distribution, to end up at a quasi-continuous uniform distribution, but not lose information.\n", "\n", - "We can use the add_jitter function for this. We can pass it the colums to add jitter to, and the amplitude of a uniform jitter. Importantly, this step should be taken in the very beginning as first step before any dataframe operations are added.\n", + "We can use the add_jitter function for this. We can pass it the columns to add jitter to, and the amplitude of a uniform jitter. Importantly, this step should be taken in the very beginning as first step before any dataframe operations are added.\n", "\n", "Let's try with a value of 0.2 for the amplitude:" ] @@ -250,7 +250,7 @@ "id": "282994b8", "metadata": {}, "source": [ - "This jittering fills the gaps, and produces a continous uniform distribution. Let's check again the longer-range binning that gave us the oscillations initially:" + "This jittering fills the gaps, and produces a continuous uniform distribution. Let's check again the longer-range binning that gave us the oscillations initially:" ] }, { @@ -275,7 +275,7 @@ "id": "bc64928b", "metadata": {}, "source": [ - "Now, the artefacts are absent, and similarly will they be in any dataframe columns derived from a column jittered in such a way. Note that this only applies to data present in digital (i.e. machine-binned) format, and not to data that are intrinsically continous. \n", + "Now, the artifacts are absent, and similarly will they be in any dataframe columns derived from a column jittered in such a way. Note that this only applies to data present in digital (i.e. machine-binned) format, and not to data that are intrinsically continuous. \n", "\n", "Also note that too large or not well-aligned jittering amplitudes will\n", "- deteriorate your resolution along the jittered axis\n", @@ -376,7 +376,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.6" + "version": "3.8.12" } }, "nbformat": 4, From 57d6cd711977f6e948093d40eecd161d2a0830ee Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 26 Jun 2024 14:53:54 +0200 Subject: [PATCH 119/300] some additional spell fixes --- ...pipeline_for_example_time-resolved_ARPES_data.ipynb | 2 +- tutorial/4_hextof_workflow.ipynb | 10 +++++----- tutorial/5_sxp_workflow.ipynb | 4 ++-- tutorial/7_correcting_orthorhombic_symmetry.ipynb | 2 +- tutorial/8_jittering_tutorial.ipynb | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index fb36f549..7b9cb1e2 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -255,7 +255,7 @@ "source": [ "### Momentum calibration workflow\n", "#### 1. Step:\n", - "First, the momentum scaling needs to be calibrated. Either, one can provide the coordinates of one point outside the center, and provide its distane to the Brillouin zone center (which is assumed to be located in the center of the image), one can specify two points on the image and their distance (where the 2nd point marks the BZ center),or one can provide absolute k-coordinates of two distinct momentum points.\n", + "First, the momentum scaling needs to be calibrated. Either, one can provide the coordinates of one point outside the center, and provide its distance to the Brillouin zone center (which is assumed to be located in the center of the image), one can specify two points on the image and their distance (where the 2nd point marks the BZ center),or one can provide absolute k-coordinates of two distinct momentum points.\n", "\n", "If no points are provided, an interactive tool is created. Here, left mouse click selects the off-center point (brillouin_zone_centered=True) or toggle-selects the off-center and center point." ] diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index cfe90f17..f428bf71 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -247,7 +247,7 @@ "metadata": {}, "source": [ "### Binning\n", - "Here we define the parameters for binning the dataframe to an n-dimensional histogram, which we can then plot, analyse or save.\n", + "Here we define the parameters for binning the dataframe to an n-dimensional histogram, which we can then plot, analyze or save.\n", "\n", "If you never saw this before, the type after `:` is a \"hint\" to what type the object to the left will have. We include them here to make sure you know what each variable should be.\n", "```python\n", @@ -417,7 +417,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now, to determine propper binning ranges, let's have again a look at the event histograms:" + "Now, to determine proper binning ranges, let's have again a look at the event histograms:" ] }, { @@ -455,7 +455,7 @@ "outputs": [], "source": [ "plt.figure()\n", - "res.plot.line(x='dldTime'); # the ; here is to suppres an annoying output" + "res.plot.line(x='dldTime'); # the ; here is to suppress an annoying output" ] }, { @@ -585,7 +585,7 @@ "metadata": {}, "source": [ "### correct offsets\n", - "The energy axis is now correct, but still the curves do not stack on eachother as we are not compensating for the `sampleBias`. In the same way, we can compensate the photon energy (`monochromatorPhotonEnergy`) and the `tofVoltage` " + "The energy axis is now correct, but still the curves do not stack on each other as we are not compensating for the `sampleBias`. In the same way, we can compensate the photon energy (`monochromatorPhotonEnergy`) and the `tofVoltage` " ] }, { @@ -656,7 +656,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A more general function, which saves parameters for all the calibrations performed. Use either the above or below function. They are equivalent (and overwrite eachother)" + "A more general function, which saves parameters for all the calibrations performed. Use either the above or below function. They are equivalent (and overwrite each other)" ] }, { diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index ea89238e..f7a89220 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -175,7 +175,7 @@ "metadata": {}, "source": [ "## Last bunch contains unusually many events \n", - "garbadge events, filter away" + "garbage events, filter away" ] }, { @@ -394,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.8.12" } }, "nbformat": 4, diff --git a/tutorial/7_correcting_orthorhombic_symmetry.ipynb b/tutorial/7_correcting_orthorhombic_symmetry.ipynb index 5d032eff..a385b3d1 100644 --- a/tutorial/7_correcting_orthorhombic_symmetry.ipynb +++ b/tutorial/7_correcting_orthorhombic_symmetry.ipynb @@ -37,7 +37,7 @@ "metadata": {}, "source": [ "## Load Data\n", - "For this example, we use the example data from WSe2. Even though the system is hexagonal, we will use it for demonstation." + "For this example, we use the example data from WSe2. Even though the system is hexagonal, we will use it for demonstration." ] }, { diff --git a/tutorial/8_jittering_tutorial.ipynb b/tutorial/8_jittering_tutorial.ipynb index 3aa5583b..ef11af7a 100644 --- a/tutorial/8_jittering_tutorial.ipynb +++ b/tutorial/8_jittering_tutorial.ipynb @@ -315,7 +315,7 @@ "id": "307557a9", "metadata": {}, "source": [ - "If the step-size of digitization is different from 1, the corresponding stepssize (half the distance between digitized values) can be adjusted as shown above.\n", + "If the step-size of digitization is different from 1, the corresponding stepsize (half the distance between digitized values) can be adjusted as shown above.\n", "\n", "Also, alternatively also normally distributed noise can be added, which is less sensitive to the exact right amplitude, but will lead to mixing of neighboring voxels, and thus loss of resolution. Also, normally distributed noise is substantially more computation-intensive to generate. It can nevertheless be helpful in situations where e.g. the stepsize is non-uniform." ] From d131fe445e54da391a759c16698f6bb7fb4affd3 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 28 Jun 2024 21:20:33 +0200 Subject: [PATCH 120/300] remove nans from all electron channels --- sed/loader/flash/buffer_handler.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index ade06975..83ce3091 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -170,7 +170,7 @@ def _fill_dataframes(self): ) self.metadata["file_statistics"] = file_metadata - channels: list[str] = get_channels( + fill_channels: list[str] = get_channels( self._config["channels"], ["per_pulse", "per_train"], extend_aux=True, @@ -180,19 +180,18 @@ def _fill_dataframes(self): dataframe = forward_fill_lazy( df=dataframe, - columns=channels, + columns=fill_channels, before=overlap, iterations=self._config.get("forward_fill_iterations", 2), ) self.metadata["forward_fill"] = { - "columns": channels, + "columns": fill_channels, "overlap": overlap, "iterations": self._config.get("forward_fill_iterations", 2), } - # Drop rows with nan values in the tof column - tof_column = self._config.get("tof_column", "dldTimeSteps") - df_electron = dataframe.dropna(subset=tof_column) + # Drop rows with nan values in electron channels + df_electron = dataframe.dropna(subset=get_channels(["per_electron"])) # Set the dtypes of the channels here as there should be no null values channel_dtypes = get_channels(self._config["channels"], "all") @@ -212,7 +211,7 @@ def _fill_dataframes(self): self.metadata.update(meta) self.df_electron = df_electron.astype(dtypes) - self.df_pulse = dataframe[index + channels] + self.df_pulse = dataframe[index + fill_channels] def run( self, From 194c8743d05aecc28b09d6a7cfa95d4b825fca97 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 28 Jun 2024 21:23:34 +0200 Subject: [PATCH 121/300] use pd import, load h5 file inside df creator --- sed/loader/flash/buffer_handler.py | 13 +--- sed/loader/flash/dataframe.py | 80 ++++++++++---------- tests/loader/flash/conftest.py | 43 ----------- tests/loader/flash/test_dataframe_creator.py | 42 +++++----- 4 files changed, 63 insertions(+), 115 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 83ce3091..e997b3bd 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -5,7 +5,6 @@ from pathlib import Path import dask.dataframe as dd -import h5py import pyarrow.parquet as pq from joblib import delayed from joblib import Parallel @@ -126,17 +125,9 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: h5_path (Path): Path to the H5 file. parquet_path (Path): Path to the buffer file. """ - # Open the h5 file in read mode - h5_file = h5py.File(h5_path, "r") - # Create a DataFrameCreator instance with the configuration and the h5 file - dfc = DataFrameCreator(config_dataframe=self._config, h5_file=h5_file) - - # Get the DataFrame from the DataFrameCreator instance - df = dfc.df - - # Close the h5 file - h5_file.close() + # Create a DataFrameCreator instance and the h5 file + df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 91dedf1e..0c207f0b 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -1,12 +1,10 @@ from __future__ import annotations +from pathlib import Path + import h5py import numpy as np -from pandas import concat -from pandas import DataFrame -from pandas import Index -from pandas import MultiIndex -from pandas import Series +import pandas as pd from sed.loader.flash.utils import get_channels @@ -16,16 +14,15 @@ class DataFrameCreator: Utility class for creating pandas DataFrames from HDF5 files with multiple channels. """ - def __init__(self, config_dataframe: dict, h5_file: h5py.File) -> None: + def __init__(self, config_dataframe: dict, h5_path: Path) -> None: """ Initializes the DataFrameCreator class. Args: config_dataframe (dict): The configuration dictionary with only the dataframe key. - h5_file (h5py.File): The open h5 file. + h5_path (Path): Path to the h5 file. """ - self.h5_file: h5py.File = h5_file - self.failed_files_error: list[str] = [] + self.h5_file = h5py.File(h5_path, "r") self.multi_index = get_channels(index=True) self._config = config_dataframe @@ -59,7 +56,7 @@ def get_dataset_array( self, channel: str, slice_: bool = False, - ) -> tuple[Index, h5py.Dataset]: + ) -> tuple[pd.Index, h5py.Dataset]: """ Returns a numpy array for a given channel name. @@ -68,13 +65,13 @@ def get_dataset_array( slice_ (bool): If True, applies slicing on the dataset. Returns: - tuple[Index, h5py.Dataset]: A tuple containing the train ID Index and the numpy array - for the channel's data. + tuple[pd.Index, h5py.Dataset]: A tuple containing the train ID + pd.Index and the numpy array for the channel's data. """ # Get the data from the necessary h5 file and channel index_key, dataset_key = self.get_index_dataset_key(channel) - key = Index(self.h5_file[index_key], name="trainId") # macrobunch + key = pd.Index(self.h5_file[index_key], name="trainId") # macrobunch dataset = self.h5_file[dataset_key] if slice_: @@ -88,7 +85,7 @@ def get_dataset_array( return key, dataset - def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: + def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: """ Computes the index for the 'per_electron' data. @@ -96,7 +93,7 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: offset (int): The offset value. Returns: - tuple[MultiIndex, np.ndarray]: A tuple containing the computed MultiIndex and + tuple[pd.MultiIndex, np.ndarray]: A tuple containing the computed pd.MultiIndex and the indexer. """ # Get the pulseId and the index_train @@ -105,8 +102,8 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) # Explode the pulse dataset and subtract by the ubid_offset pulse_ravel = dataset_pulse.ravel() - offset - # Create a MultiIndex with the index_train and the pulse - microbunches = MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() + # Create a pd.MultiIndex with the index_train and the pulse + microbunches = pd.MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() # Only sort if necessary indexer = slice(None) @@ -118,7 +115,7 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: electrons = np.concatenate([np.arange(count) for count in electron_counts]) # Final index constructed here - index = MultiIndex.from_arrays( + index = pd.MultiIndex.from_arrays( ( microbunches.get_level_values(0), microbunches.get_level_values(1).astype(int), @@ -129,15 +126,15 @@ def pulse_index(self, offset: int) -> tuple[MultiIndex, slice | np.ndarray]: return index, indexer @property - def df_electron(self) -> DataFrame: + def df_electron(self) -> pd.DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per electron]. Returns: - DataFrame: The pandas DataFrame for the 'per_electron' channel's data. + pd.DataFrame: The pandas DataFrame for the 'per_electron' channel's data. """ offset = self._config["ubid_offset"] - # Index + # pd.Index index, indexer = self.pulse_index(offset) # Data logic @@ -155,21 +152,21 @@ def df_electron(self) -> DataFrame: channel: dataset[:, slice_, :].ravel() for channel, slice_ in zip(channels, slice_index) } - dataframe = DataFrame(data_dict) - # Otherwise, we need to create a Series for each channel and concatenate them + dataframe = pd.DataFrame(data_dict) + # Otherwise, we need to create a pd.Series for each channel and concatenate them else: series = { - channel: Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + channel: pd.Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) for channel in channels } - dataframe = concat(series, axis=1) + dataframe = pd.concat(series, axis=1) drop_vals = np.arange(-offset, 0) # Few things happen here: # Drop all NaN values like while creating the multiindex # if necessary, the data is sorted with [indexer] - # MultiIndex is set + # pd.MultiIndex is set # Finally, the offset values are dropped return ( dataframe.dropna() @@ -179,33 +176,36 @@ def df_electron(self) -> DataFrame: ) @property - def df_pulse(self) -> DataFrame: + def df_pulse(self) -> pd.DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per pulse]. Returns: - DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. + pd.DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. """ series = [] channels = get_channels(self._config["channels"], "per_pulse") for channel in channels: # get slice key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( + index = pd.MultiIndex.from_product( (key, np.arange(0, dataset.shape[1]), [0]), names=self.multi_index, ) - series.append(Series(dataset[()].ravel(), index=index, name=channel)) + series.append(pd.Series(dataset[()].ravel(), index=index, name=channel)) - return concat(series, axis=1) # much faster when concatenating similarly indexed data first + return pd.concat( + series, + axis=1, + ) # much faster when concatenating similarly indexed data first @property - def df_train(self) -> DataFrame: + def df_train(self) -> pd.DataFrame: """ Returns a pandas DataFrame for a given channel name of type [per train]. Returns: - DataFrame: The pandas DataFrame for the 'per_train' channel's data. + pd.DataFrame: The pandas DataFrame for the 'per_train' channel's data. """ series = [] @@ -213,18 +213,18 @@ def df_train(self) -> DataFrame: for channel in channels: key, dataset = self.get_dataset_array(channel, slice_=True) - index = MultiIndex.from_product( + index = pd.MultiIndex.from_product( (key, [0], [0]), names=self.multi_index, ) if channel == "dldAux": aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() for name, slice_aux in aux_channels: - series.append(Series(dataset[: key.size, slice_aux], index, name=name)) + series.append(pd.Series(dataset[: key.size, slice_aux], index, name=name)) else: - series.append(Series(dataset, index, name=channel)) + series.append(pd.Series(dataset, index, name=channel)) - return concat(series, axis=1) + return pd.concat(series, axis=1) def validate_channel_keys(self) -> None: """ @@ -236,18 +236,18 @@ def validate_channel_keys(self) -> None: for channel in self._config["channels"]: index_key, dataset_key = self.get_index_dataset_key(channel) if index_key not in self.h5_file: - raise KeyError(f"Index key '{index_key}' doesn't exist in the file.") + raise KeyError(f"pd.Index key '{index_key}' doesn't exist in the file.") if dataset_key not in self.h5_file: raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") @property - def df(self) -> DataFrame: + def df(self) -> pd.DataFrame: """ Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, returning a single dataframe. Returns: - DataFrame: The combined pandas DataFrame. + pd.DataFrame: The combined pandas DataFrame. """ self.validate_channel_keys() diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 9c27a216..7cec83d8 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -72,46 +72,3 @@ def fixture_h5_paths(): return [ Path(os.path.join(package_dir, f"../tests/data/loader/flash/{path}")) for path in H5_PATHS ] - - -# @pytest.fixture(name="pulserSignAdc_channel_array") -# def get_pulse_channel_from_h5(config_dataframe, h5_file): -# df = DataFrameCreator(config_dataframe) -# df.h5_file = h5_file -# train_id, pulse_id = df.get_dataset_array("pulserSignAdc") -# return train_id, pulse_id - - -# @pytest.fixture(name="multiindex_electron") -# def fixture_multi_index_electron(config_dataframe, h5_file): -# """Fixture providing multi index for electron resolved data""" -# df = DataFrameCreator(config_dataframe) -# df.h5_file = h5_file -# pulse_index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) - -# return pulse_index, indexer - - -# @pytest.fixture(name="fake_data") -# def fake_data_electron(): -# # Creating manageable fake data, but not used currently -# num_trains = 5 -# max_pulse_id = 100 -# nan_threshold = 50 -# ubid_offset = 5 -# seed = 42 -# np.random.seed(seed) -# train_ids = np.arange(1600000000, 1600000000 + num_trains) -# fake_data = [] - -# for _ in train_ids: -# pulse_ids = [] -# while len(pulse_ids) < nan_threshold: -# random_pulse_ids = np.random.choice( -# np.arange(ubid_offset, nan_threshold), size=np.random.randint(0, 10)) -# pulse_ids = np.concatenate([pulse_ids, random_pulse_ids]) - -# pulse_ids = np.concatenate([pulse_ids, np.full(max_pulse_id-len(pulse_ids), np.nan)]) - -# fake_data.append(np.sort(pulse_ids)) -# return Series(train_ids, name="trainId"), np.array(fake_data), ubid_offset diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 3fbe37ca..c318d577 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -10,11 +10,11 @@ from sed.loader.flash.utils import get_channels -def test_get_index_dataset_key(config_dataframe, h5_file): +def test_get_index_dataset_key(config_dataframe, h5_paths): """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" - df = DataFrameCreator(config, h5_file) + df = DataFrameCreator(config, h5_paths[0]) index_key, dataset_key = df.get_index_dataset_key(channel) assert index_key == config["channels"][channel]["index_key"] assert dataset_key == config["channels"][channel]["dataset_key"] @@ -25,10 +25,10 @@ def test_get_index_dataset_key(config_dataframe, h5_file): df.get_index_dataset_key(channel) -def test_get_dataset_array(config_dataframe, h5_file): +def test_get_dataset_array(config_dataframe, h5_paths): """Test the creation of a h5py dataset for a given channel.""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) channel = "dldPosX" train_id, dset = df.get_dataset_array(channel) @@ -50,11 +50,11 @@ def test_get_dataset_array(config_dataframe, h5_file): assert dset.shape[1] == 500 -def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): +def test_empty_get_dataset_array(config_dataframe, h5_paths, h5_file_copy): """Test the method when given an empty dataset.""" channel = "gmdTunnel" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) train_id, dset = df.get_dataset_array(channel) channel_index_key = "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index" @@ -69,7 +69,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): shape=(train_id.shape[0], 0), ) - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) df.h5_file = h5_file_copy train_id, dset_empty = df.get_dataset_array(channel) @@ -78,10 +78,10 @@ def test_empty_get_dataset_array(config_dataframe, h5_file, h5_file_copy): assert dset_empty.shape[1] == 0 -def test_pulse_index(config_dataframe, h5_file): +def test_pulse_index(config_dataframe, h5_paths): """Test the creation of the pulse index for electron resolved data""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) pulse_index, pulse_array = df.get_dataset_array("pulseId", slice_=True) index, indexer = df.pulse_index(config_dataframe["ubid_offset"]) # Check if the index_per_electron is a MultiIndex and has the correct levels @@ -113,9 +113,9 @@ def test_pulse_index(config_dataframe, h5_file): assert index.is_monotonic_increasing -def test_df_electron(config_dataframe, h5_file): +def test_df_electron(config_dataframe, h5_paths): """Test the creation of a pandas DataFrame for a channel of type [per electron].""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df_electron @@ -151,9 +151,9 @@ def test_df_electron(config_dataframe, h5_file): ) -def test_create_dataframe_per_pulse(config_dataframe, h5_file): +def test_create_dataframe_per_pulse(config_dataframe, h5_paths): """Test the creation of a pandas DataFrame for a channel of type [per pulse].""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df_pulse # Check that the result_df is a DataFrame and has the correct shape assert isinstance(result_df, DataFrame) @@ -181,9 +181,9 @@ def test_create_dataframe_per_pulse(config_dataframe, h5_file): ) -def test_create_dataframe_per_train(config_dataframe, h5_file): +def test_create_dataframe_per_train(config_dataframe, h5_paths): """Test the creation of a pandas DataFrame for a channel of type [per train].""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df_train channel = "delayStage" @@ -230,20 +230,20 @@ def test_create_dataframe_per_train(config_dataframe, h5_file): assert result_df.index.is_unique -def test_group_name_not_in_h5(config_dataframe, h5_file): +def test_group_name_not_in_h5(config_dataframe, h5_paths): """Test ValueError when the group_name for a channel does not exist in the H5 file.""" channel = "dldPosX" config = config_dataframe config["channels"][channel]["dataset_key"] = "foo" - df = DataFrameCreator(config, h5_file) + df = DataFrameCreator(config, h5_paths[0]) with pytest.raises(KeyError): df.df_electron -def test_create_dataframe_per_file(config_dataframe, h5_file): +def test_create_dataframe_per_file(config_dataframe, h5_paths): """Test the creation of pandas DataFrames for a given file.""" - df = DataFrameCreator(config_dataframe, h5_file) + df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df # Check that the result_df is a DataFrame and has the correct shape @@ -253,11 +253,11 @@ def test_create_dataframe_per_file(config_dataframe, h5_file): assert result_df.shape[0] == len(all_keys.unique()) -def test_get_index_dataset_key_error(config_dataframe, h5_file): +def test_get_index_dataset_key_error(config_dataframe, h5_paths): """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" - df = DataFrameCreator(config, h5_file) + df = DataFrameCreator(config, h5_paths[0]) del config["channels"][channel]["dataset_key"] with pytest.raises(ValueError): From af33740c505886f48f50926c21e0f7cd514a4aa5 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 28 Jun 2024 22:24:58 +0200 Subject: [PATCH 122/300] update comments to explain the code --- sed/loader/flash/dataframe.py | 106 +++++++++++++++++++++++----------- sed/loader/flash/utils.py | 1 + 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 0c207f0b..99967ea3 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -1,3 +1,9 @@ +""" +This module creates pandas DataFrames from HDF5 files for different levels of data granularity +[per electron, per pulse, and per train]. It efficiently handles concatenation of data from +various channels within the HDF5 file, making use of the structured nature data to optimize +join operations. This approach significantly enhances performance compared to earlier. +""" from __future__ import annotations from pathlib import Path @@ -11,7 +17,12 @@ class DataFrameCreator: """ - Utility class for creating pandas DataFrames from HDF5 files with multiple channels. + A class for creating pandas DataFrames from an HDF5 file. + + Attributes: + h5_file (h5py.File): The HDF5 file object. + multi_index (pd.MultiIndex): The multi-index structure for the DataFrame. + _config (dict): The configuration dictionary for the DataFrame. """ def __init__(self, config_dataframe: dict, h5_path: Path) -> None: @@ -85,9 +96,10 @@ def get_dataset_array( return key, dataset - def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: + def pulse_dataset(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: """ - Computes the index for the 'per_electron' data. + Creates a multi-level index that combines train IDs and pulse IDs, and handles + sorting and electron counting within each pulse. Args: offset (int): The offset value. @@ -96,30 +108,35 @@ def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: tuple[pd.MultiIndex, np.ndarray]: A tuple containing the computed pd.MultiIndex and the indexer. """ - # Get the pulseId and the index_train - index_train, dataset_pulse = self.get_dataset_array("pulseId", slice_=True) - # Repeat the index_train by the number of pulses - index_train_repeat = np.repeat(index_train, dataset_pulse.shape[1]) - # Explode the pulse dataset and subtract by the ubid_offset - pulse_ravel = dataset_pulse.ravel() - offset - # Create a pd.MultiIndex with the index_train and the pulse - microbunches = pd.MultiIndex.from_arrays((index_train_repeat, pulse_ravel)).dropna() - - # Only sort if necessary + # Get the pulse_dataset and the train_index + train_index, pulse_dataset = self.get_dataset_array("pulseId", slice_=True) + # pulse_dataset comes as a 2D array, resolved per train. Here it is flattened + # the daq has an offset so no pulses are missed. This offset is subtracted here + pulse_ravel = pulse_dataset.ravel() - offset + # Here train_index is repeated to match the size of pulses + train_index_repeated = np.repeat(train_index, pulse_dataset.shape[1]) + # A pulse resolved multi-index is finally created. + # Since there can be NaN pulses, those are dropped + pulse_index = pd.MultiIndex.from_arrays((train_index_repeated, pulse_ravel)).dropna() + + # Sometimes the pulse_index are not monotonic, so we might need to sort them + # The indexer is also returned to sort the data in df_electron indexer = slice(None) - if not microbunches.is_monotonic_increasing: - microbunches, indexer = microbunches.sort_values(return_indexer=True) + if not pulse_index.is_monotonic_increasing: + pulse_index, indexer = pulse_index.sort_values(return_indexer=True) - # Count the number of electrons per microbunch and create an array of electrons - electron_counts = microbunches.value_counts(sort=False).values - electrons = np.concatenate([np.arange(count) for count in electron_counts]) + # In the data, to signify different electrons, pulse_index is repeated by + # the number of electrons in each pulse. Here the values are counted + electron_counts = pulse_index.value_counts(sort=False).values + # Now we resolve each pulse to its electrons + electron_index = np.concatenate([np.arange(count) for count in electron_counts]) - # Final index constructed here + # Final multi-index constructed here index = pd.MultiIndex.from_arrays( ( - microbunches.get_level_values(0), - microbunches.get_level_values(1).astype(int), - electrons, + pulse_index.get_level_values(0), + pulse_index.get_level_values(1).astype(int), + electron_index, ), names=self.multi_index, ) @@ -128,24 +145,27 @@ def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: @property def df_electron(self) -> pd.DataFrame: """ - Returns a pandas DataFrame for a given channel name of type [per electron]. + Returns a pandas DataFrame for channel names of type [per electron]. Returns: pd.DataFrame: The pandas DataFrame for the 'per_electron' channel's data. """ offset = self._config["ubid_offset"] - # pd.Index - index, indexer = self.pulse_index(offset) + # Here we get the multi-index and the indexer to sort the data + index, indexer = self.pulse_dataset(offset) - # Data logic + # Get the relevant channels and their slice index channels = get_channels(self._config["channels"], "per_electron") slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] # First checking if dataset keys are the same for all channels + # because DLD at FLASH stores all channels in the same h5 dataset dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] + # Gives a true if all keys are the same all_keys_same = all(key == dataset_keys[0] for key in dataset_keys) - # If all dataset keys are the same, we can directly use the ndarray to create frame + # If all dataset keys are the same, we only need to load the dataset once and slice + # the appropriate columns. This is much faster than loading the same dataset multiple times if all_keys_same: _, dataset = self.get_dataset_array(channels[0]) data_dict = { @@ -153,7 +173,7 @@ def df_electron(self) -> pd.DataFrame: for channel, slice_ in zip(channels, slice_index) } dataframe = pd.DataFrame(data_dict) - # Otherwise, we need to create a pd.Series for each channel and concatenate them + # In case channels do differ, we create a pd.Series for each channel and concatenate them else: series = { channel: pd.Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) @@ -161,6 +181,7 @@ def df_electron(self) -> pd.DataFrame: } dataframe = pd.concat(series, axis=1) + # after offset, the negative pulse values are dropped as they are not valid drop_vals = np.arange(-offset, 0) # Few things happen here: @@ -178,52 +199,67 @@ def df_electron(self) -> pd.DataFrame: @property def df_pulse(self) -> pd.DataFrame: """ - Returns a pandas DataFrame for a given channel name of type [per pulse]. + Returns a pandas DataFrame for given channel names of type [per pulse]. Returns: pd.DataFrame: The pandas DataFrame for the 'per_pulse' channel's data. """ series = [] + # Get the relevant channel names channels = get_channels(self._config["channels"], "per_pulse") + # For each channel, a pd.Series is created and appended to the list for channel in channels: - # get slice + # train_index and (sliced) data is returned key, dataset = self.get_dataset_array(channel, slice_=True) + # Electron resolved MultiIndex is created. Since this is pulse data, + # the electron index is always 0 index = pd.MultiIndex.from_product( (key, np.arange(0, dataset.shape[1]), [0]), names=self.multi_index, ) + # The dataset is opened and converted to numpy array by [()] + # and flattened to resolve per pulse + # The pd.Series is created with the MultiIndex and appended to the list series.append(pd.Series(dataset[()].ravel(), index=index, name=channel)) + # All the channels are concatenated to a single DataFrame return pd.concat( series, axis=1, - ) # much faster when concatenating similarly indexed data first + ) @property def df_train(self) -> pd.DataFrame: """ - Returns a pandas DataFrame for a given channel name of type [per train]. + Returns a pandas DataFrame for given channel names of type [per train]. Returns: pd.DataFrame: The pandas DataFrame for the 'per_train' channel's data. """ series = [] - + # Get the relevant channel names channels = get_channels(self._config["channels"], "per_train") - + # For each channel, a pd.Series is created and appended to the list for channel in channels: + # train_index and (sliced) data is returned key, dataset = self.get_dataset_array(channel, slice_=True) + # Electron and pulse resolved MultiIndex is created. Since this is train data, + # the electron and pulse index is always 0 index = pd.MultiIndex.from_product( (key, [0], [0]), names=self.multi_index, ) + # Auxillary dataset (which is stored in the same dataset as other DLD channels) + # contains multiple channels inside. Even though they are resolved per train, + # they come in pulse format, so the extra values are sliced and individual channels are + # created and appended to the list if channel == "dldAux": aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() for name, slice_aux in aux_channels: series.append(pd.Series(dataset[: key.size, slice_aux], index, name=name)) else: series.append(pd.Series(dataset, index, name=channel)) - + # All the channels are concatenated to a single DataFrame return pd.concat(series, axis=1) def validate_channel_keys(self) -> None: diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index cae76168..8b88ee7d 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -2,6 +2,7 @@ from pathlib import Path +# TODO: move to config MULTI_INDEX = ["trainId", "pulseId", "electronId"] PULSE_ALIAS = MULTI_INDEX[1] DLD_AUX_ALIAS = "dldAux" From 50f7ee1e92a2af5d331fe474ecd006736926f042 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 28 Jun 2024 23:47:11 +0200 Subject: [PATCH 123/300] make review changes --- sed/loader/flash/buffer_handler.py | 2 +- sed/loader/flash/dataframe.py | 6 +++--- sed/loader/flash/loader.py | 13 +++++++------ sed/loader/flash/utils.py | 13 +++++++++++-- sed/loader/utils.py | 8 ++++---- tests/loader/flash/test_flash_loader.py | 16 ++++++++-------- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index e997b3bd..e4e6b879 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -119,7 +119,7 @@ def _get_files_to_read( def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: """ - Creates a single buffer file. Useful because h5py.File cannot be pickled if left open. + Creates a single buffer file. Args: h5_path (Path): Path to the H5 file. diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 99967ea3..86519740 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -96,7 +96,7 @@ def get_dataset_array( return key, dataset - def pulse_dataset(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: + def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: """ Creates a multi-level index that combines train IDs and pulse IDs, and handles sorting and electron counting within each pulse. @@ -150,9 +150,9 @@ def df_electron(self) -> pd.DataFrame: Returns: pd.DataFrame: The pandas DataFrame for the 'per_electron' channel's data. """ - offset = self._config["ubid_offset"] + offset = self._config.get("ubid_offset", 5) # 5 is the default value # Here we get the multi-index and the indexer to sort the data - index, indexer = self.pulse_dataset(offset) + index, indexer = self.pulse_index(offset) # Get the relevant channels and their slice index channels = get_channels(self._config["channels"], "per_electron") diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 2428fd74..01d2aa62 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -131,7 +131,7 @@ def get_files_from_run_id( # type: ignore[override] for the specified data acquisition (daq). Args: - run_id (str): The run identifier to locate. + run_id (str | int): The run identifier to locate. folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"]. extension (str, optional): The file extension. Defaults to "h5". @@ -198,7 +198,7 @@ def get_count_rate( ): return None, None - def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 + def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[float]: # type: ignore[override] """ Calculates the elapsed time. @@ -210,7 +210,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 the specified files or the elapsed time for each file. Defaults to True. Returns: - Union[float, List[float]]: The elapsed time(s) in seconds. + float | list[float]: The elapsed time(s) in seconds. Raises: KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. @@ -224,6 +224,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds): # noqa: ARG002 def get_elapsed_time_from_fid(fid): try: + fid = str(fid) # Ensure the key is a string time_stamps = file_statistics[fid]["time_stamps"] elapsed_time = max(time_stamps) - min(time_stamps) except KeyError as exc: @@ -277,9 +278,9 @@ def read_dataframe( folders (str | Sequence[str], optional): Path to folder(s) where files are stored Path has priority such that if it's specified, the specified files will be ignored. Defaults to None. - runs (str | Sequence[str], optional): Run identifier(s). Corresponding files will - be located in the location provided by ``folders``. Takes precedence over - ``files`` and ``folders``. Defaults to None. + runs (str | int | Sequence[str | int], optional): Run identifier(s). + Corresponding files will be located in the location provided by ``folders``. + Takes precedence over ``files`` and ``folders``. Defaults to None. ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 8b88ee7d..76af4150 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -20,7 +20,7 @@ def get_channels( Returns a list of channels associated with the specified format(s). Args: - formats (Union[str, List[str]]): The desired format(s) + formats (str | list[str]): The desired format(s) ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multiindex. extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, @@ -99,7 +99,16 @@ def initialize_paths( the specified parameters during initialization. Args: - paths (List[Path]): Optional custom paths for the Parquet files. + filenames (str | list[str]): The name(s) of the file(s). + folder (Path): The folder where the files are saved. + subfolder (str): The subfolder where the files are saved. + prefix (str): The prefix for the file name. + suffix (str): The suffix for the file name. + extension (str): The extension for the file. + paths (list[Path]): Custom paths for the files. + + Returns: + list[Path]: The paths for the files. """ # if filenames is string, convert it to a list if isinstance(filenames, str): diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 7e16720a..f47419c0 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -227,7 +227,7 @@ def get_timestamp_stats(meta: pq.FileMetaData, time_stamp_col: str) -> tuple[int return min(timestamps), max(timestamps) -def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[int, dict]: +def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[str, dict]: """ Extracts and organizes metadata from a list of Parquet files. @@ -235,11 +235,11 @@ def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[in extract the minimum and maximum timestamps. "row_groups" entry is removed from FileMetaData. Args: - file_paths (List[Path]): A list of paths to the Parquet files. + file_paths (list[Path]): A list of paths to the Parquet files. time_stamp_col (str): The name of the column containing the timestamps. Returns: - Dict[str, Dict]: A dictionary file index as key and the values as metadata of each file. + dict[str, dict]: A dictionary file index as key and the values as metadata of each file. """ organized_metadata = {} for i, file_path in enumerate(file_paths): @@ -261,6 +261,6 @@ def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[in metadata_dict.pop("row_groups", None) # Add the metadata dictionary to the organized_metadata dictionary - organized_metadata[i] = metadata_dict + organized_metadata[str(i)] = metadata_dict return organized_metadata diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index b9cb987b..f620e230 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -110,9 +110,9 @@ def test_get_elapsed_time_fid(config_file): # Mock the file_statistics and files fl.metadata = { "file_statistics": { - 0: {"time_stamps": [10, 20]}, - 1: {"time_stamps": [20, 30]}, - 2: {"time_stamps": [30, 40]}, + "0": {"time_stamps": [10, 20]}, + "1": {"time_stamps": [20, 30]}, + "2": {"time_stamps": [30, 40]}, }, } fl.files = ["file0", "file1", "file2"] @@ -136,8 +136,8 @@ def test_get_elapsed_time_fid(config_file): # Test KeyError when time_stamps is missing fl.metadata = { "file_statistics": { - 0: {}, - 1: {"time_stamps": [20, 30]}, + "0": {}, + "1": {"time_stamps": [20, 30]}, }, } with pytest.raises(KeyError) as e: @@ -157,9 +157,9 @@ def test_get_elapsed_time_run(config_file): fl = FlashLoader(config=config_file) fl.read_dataframe(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"][0]["time_stamps"] + start, end = fl.metadata["file_statistics"]["0"]["time_stamps"] expected_elapsed_time_0 = end - start - start, end = fl.metadata["file_statistics"][1]["time_stamps"] + start, end = fl.metadata["file_statistics"]["1"]["time_stamps"] expected_elapsed_time_1 = end - start elapsed_time = fl.get_elapsed_time(runs=[43878]) @@ -169,7 +169,7 @@ def test_get_elapsed_time_run(config_file): assert elapsed_time == [expected_elapsed_time_0, expected_elapsed_time_1] elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"][1]["time_stamps"] + start, end = fl.metadata["file_statistics"]["1"]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 From 65d909da3d753d096234cdd687c3d009f2287084 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 29 Jun 2024 00:52:04 +0200 Subject: [PATCH 124/300] fix tests with review comments --- tests/loader/flash/test_buffer_handler.py | 36 +++++- tests/loader/flash/test_dataframe_creator.py | 49 +++++---- tests/loader/flash/test_flash_loader.py | 110 ++++++++++++------- tests/loader/flash/test_utils.py | 10 +- 4 files changed, 133 insertions(+), 72 deletions(-) diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 822e3c04..7685d8ec 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -1,3 +1,4 @@ +"""Test cases for the BufferHandler class in the Flash module.""" from pathlib import Path import numpy as np @@ -9,6 +10,11 @@ def create_parquet_dir(config, folder): + """ + Creates a directory for storing Parquet files based on the provided configuration + and folder name. + """ + parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) parquet_path = parquet_path.joinpath(folder) parquet_path.mkdir(parents=True, exist_ok=True) @@ -16,20 +22,36 @@ def create_parquet_dir(config, folder): def test_get_files_to_read(config, h5_paths): + """ + Test the BufferHandler's ability to identify files that need to be read and + manage buffer file paths. + + This test performs several checks to ensure the BufferHandler correctly identifies + which HDF5 files need to be read and properly manages the paths for saving buffer + files. It follows these steps: + 1. Creates a directory structure for storing buffer files and initializes the BufferHandler. + 2. Invokes the private method _get_files_to_read to populate the list of missing HDF5 files and + verify that initially, all provided files are considered missing. + 3. Checks that the paths for saving buffer files are correctly generated. + 4. Creates a single buffer file and reruns the check to ensure that the BufferHandler recognizes + one less missing file. + 5. Cleans up by removing the created buffer file. + 6. Tests the handling of prefix and suffix in buffer file names by rerunning the checks with + modified file name parameters. + """ folder = create_parquet_dir(config, "get_files_to_read") subfolder = folder.joinpath("buffer") # set to false to avoid creating buffer files unnecessarily bh = BufferHandler(config) bh._get_files_to_read(h5_paths, folder, "", "", False) - assert len(bh.missing_h5_files) == len(h5_paths) - assert len(bh.save_paths) == len(h5_paths) - + # check that all files are to be read assert np.all(bh.missing_h5_files == h5_paths) # create expected paths expected_buffer_paths = [Path(subfolder, f"{Path(path).stem}") for path in h5_paths] + # check that all buffer paths are correct assert np.all(bh.save_paths == expected_buffer_paths) # create only one buffer file @@ -111,6 +133,14 @@ def test_buffer_schema_mismatch(config, h5_paths): def test_save_buffer_files(config, h5_paths): + """ + Test the BufferHandler's ability to save buffer files serially and in parallel. + + This test ensures that the BufferHandler can run both serially and in parallel, saving the + output to buffer files, and then it compares the resulting DataFrames to ensure they are + identical. This verifies that parallel processing does not affect the integrity of the data + saved. After the comparison, it cleans up by removing the created buffer files. + """ folder_serial = create_parquet_dir(config, "save_buffer_files_serial") bh_serial = BufferHandler(config) bh_serial.run(h5_paths, folder_serial, debug=True) diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index c318d577..6125e010 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -125,19 +125,20 @@ def test_df_electron(config_dataframe, h5_paths): # check that there are no nan values in the dataframe assert ~result_df.isnull().values.any() - # Check that the values are dropped for pulseId index below 0 (ubid_offset) - print( - np.all( - result_df.values[:5] - != np.array( - [ - [556.0, 731.0, 42888.0], - [549.0, 737.0, 42881.0], - [671.0, 577.0, 39181.0], - [671.0, 579.0, 39196.0], - [714.0, 859.0, 37530.0], - ], - ), + # Check if first 5 values are as expected + # e.g. that the values are dropped for pulseId index below 0 (ubid_offset) + # however in this data the lowest value is 9 and offset was 5 so no values are dropped + assert np.all( + result_df.values[:5] + == np.array( + [ + [556.0, 731.0, 42888.0], + [549.0, 737.0, 42881.0], + [671.0, 577.0, 39181.0], + [671.0, 579.0, 39196.0], + [714.0, 859.0, 37530.0], + ], + dtype=np.float32, ), ) assert np.all(result_df.index.get_level_values("pulseId") >= 0) @@ -168,10 +169,10 @@ def test_create_dataframe_per_pulse(config_dataframe, h5_paths): assert np.all(result_df.index.get_level_values("electronId") == 0) # pulse ids should span 0-499 on each train - assert np.all( - result_df.loc[1648851402].index.get_level_values("pulseId").values == np.arange(500), - ) - + for train_id in result_df.index.get_level_values("trainId"): + assert np.all( + result_df.loc[train_id].index.get_level_values("pulseId").values == np.arange(500), + ) # assert index uniqueness assert result_df.index.is_unique @@ -193,7 +194,7 @@ def test_create_dataframe_per_train(config_dataframe, h5_paths): assert isinstance(result_df, DataFrame) # check that all values are in the df for delayStage - np.all(result_df[channel].dropna() == data[()]) + assert np.all(result_df[channel].dropna() == data[()]) # check that dataframe contains all channels assert np.all( @@ -201,11 +202,15 @@ def test_create_dataframe_per_train(config_dataframe, h5_paths): == set(get_channels(config_dataframe["channels"], ["per_train"], extend_aux=True)), ) - # find unique index values among all per_train channels + # Ensure DataFrame has rows equal to unique keys from "per_train" channels, considering + # different channels may have data for different trains. This checks the DataFrame's + # completeness and integrity, especially important when channels record at varying trains. channels = get_channels(config_dataframe["channels"], ["per_train"]) all_keys = Index([]) for channel in channels: + # Append unique keys from each channel, considering only training data all_keys = all_keys.append(df.get_dataset_array(channel, slice_=True)[0]) + # Verify DataFrame's row count matches unique train IDs count across channels assert result_df.shape[0] == len(all_keys.unique()) # check index levels @@ -225,7 +230,7 @@ def test_create_dataframe_per_train(config_dataframe, h5_paths): # hence the slicing subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] for subchannel, index in subchannels.items(): - np.all(df.df_train[subchannel].dropna().values == data[: key.size, index]) + assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, index]) assert result_df.index.is_unique @@ -254,7 +259,9 @@ def test_create_dataframe_per_file(config_dataframe, h5_paths): def test_get_index_dataset_key_error(config_dataframe, h5_paths): - """Test the creation of the index and dataset keys for a given channel.""" + """ + Test that a ValueError is raised when the dataset_key is missing for a channel in the config. + """ config = config_dataframe channel = "dldPosX" df = DataFrameCreator(config, h5_paths[0]) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index f620e230..4deacded 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -8,7 +8,7 @@ import pytest -from sed.core.config import parse_config +from .test_buffer_handler import create_parquet_dir from sed.loader.flash.loader import FlashLoader package_dir = os.path.dirname(find_spec("sed").origin) @@ -16,22 +16,12 @@ H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" -@pytest.fixture(name="config_file") -def fixture_config_file() -> dict: - """Fixture providing a configuration file for FlashLoader tests. - - Returns: - dict: The parsed configuration file. - """ - return parse_config(config_path) - - @pytest.mark.parametrize( "sub_dir", ["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], ) def test_initialize_dirs( - config_file: dict, + config: dict, fs, sub_dir: Literal["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"], ) -> None: @@ -42,15 +32,15 @@ def test_initialize_dirs( fs: A fixture for a fake file system. sub_dir (Literal["online-0/fl1user3/", "express-0/fl1user3/", "FL1USER3/"]): Sub-directory. """ - config = config_file - del config["core"]["paths"] - config["core"]["beamtime_id"] = "12345678" - config["core"]["year"] = "2000" + config_ = config.copy() + del config_["core"]["paths"] + config_["core"]["beamtime_id"] = "12345678" + config_["core"]["year"] = "2000" # Find base path of beamline from config. Here, we use pg2 - base_path = config["dataframe"]["beamtime_dir"]["pg2"] + base_path = config_["dataframe"]["beamtime_dir"]["pg2"] expected_path = ( - Path(base_path) / config["core"]["year"] / "data" / config["core"]["beamtime_id"] + Path(base_path) / config_["core"]["year"] / "data" / config_["core"]["beamtime_id"] ) # Create expected paths expected_raw_path = expected_path / "raw" / "hdf" / sub_dir @@ -61,51 +51,93 @@ def test_initialize_dirs( fs.create_dir(expected_processed_path) # Instance of class with correct config and call initialize_dirs - fl = FlashLoader(config=config) + fl = FlashLoader(config=config_) fl._initialize_dirs() assert str(expected_raw_path) == fl.raw_dir assert str(expected_processed_path) == fl.parquet_dir # remove breamtimeid, year and daq from config to raise error - del config["core"]["beamtime_id"] + del config_["core"]["beamtime_id"] with pytest.raises(ValueError) as e: fl._initialize_dirs() print(e.value) assert "The beamtime_id and year are required." in str(e.value) -def test_initialize_dirs_filenotfound(config_file: dict) -> None: +def test_initialize_dirs_filenotfound(config: dict) -> None: """ Test FileNotFoundError during the initialization of paths. """ # Test the FileNotFoundError - config = config_file - del config["core"]["paths"] - config["core"]["beamtime_id"] = "11111111" - config["core"]["year"] = "2000" + config_ = config.copy() + del config_["core"]["paths"] + config_["core"]["beamtime_id"] = "11111111" + config_["core"]["year"] = "2000" # Instance of class with correct config and call initialize_dirs with pytest.raises(FileNotFoundError): - fl = FlashLoader(config=config) + fl = FlashLoader(config=config_) fl._initialize_dirs() def test_save_read_parquet_flash(config): - """Test ParquetHandler save and read parquet""" - config_ = config - config_["core"]["paths"]["data_parquet_dir"] = ( - config_["core"]["paths"]["data_parquet_dir"] + "_flash_save_read/" - ) + """ + Test the functionality of saving and reading parquet files with FlashLoader. + + This test performs three main actions: + 1. First call to create and read parquet files. Verifies new files are created. + 2. Second call with the same parameters to check that it only reads from + the existing parquet files without creating new ones. It asserts that the files' modification + times remain unchanged, indicating no new files were created or existing files overwritten. + 3. Third call with `force_recreate=True` to force the recreation of parquet files. + It verifies that the files were indeed overwritten by checking that their modification + times have changed. + """ + config_ = config.copy() + data_parquet_dir = create_parquet_dir(config_, "flash_save_read") + config_["core"]["paths"]["data_parquet_dir"] = data_parquet_dir fl = FlashLoader(config=config_) + + # First call: should create and read the parquet file df1, _, _ = fl.read_dataframe(runs=[43878, 43879]) + # Check if new files were created + data_parquet_dir = data_parquet_dir.joinpath("buffer") + new_files = { + file: os.path.getmtime(data_parquet_dir.joinpath(file)) + for file in os.listdir(data_parquet_dir) + } + assert new_files + # Second call: should only read the parquet file, not create new ones df2, _, _ = fl.read_dataframe(runs=[43878, 43879]) + # Verify no new files were created after the second call + final_files = { + file: os.path.getmtime(data_parquet_dir.joinpath(file)) + for file in os.listdir(data_parquet_dir) + } + assert ( + new_files == final_files + ), "Files were overwritten or new files were created after the second call." + + # Third call: We force_recreate the parquet files + df3, _, _ = fl.read_dataframe(runs=[43878, 43879], force_recreate=True) + + # Verify files were overwritten + new_files = { + file: os.path.getmtime(data_parquet_dir.joinpath(file)) + for file in os.listdir(data_parquet_dir) + } + assert new_files != final_files, "Files were not overwritten after the third call." + + # remove the parquet files + [data_parquet_dir.joinpath(file).unlink() for file in new_files] + -def test_get_elapsed_time_fid(config_file): +def test_get_elapsed_time_fid(config): """Test get_elapsed_time method of FlashLoader class""" # Create an instance of FlashLoader - fl = FlashLoader(config=config_file) + fl = FlashLoader(config=config) # Mock the file_statistics and files fl.metadata = { @@ -146,15 +178,15 @@ def test_get_elapsed_time_fid(config_file): assert "Timestamp metadata missing in file 0" in str(e.value) -def test_get_elapsed_time_run(config_file): - config = config_file - config["core"]["paths"] = { +def test_get_elapsed_time_run(config): + config_ = config.copy() + config_["core"]["paths"] = { "data_raw_dir": "tests/data/loader/flash/", "data_parquet_dir": "tests/data/loader/flash/parquet/get_elapsed_time_run", } """Test get_elapsed_time method of FlashLoader class""" # Create an instance of FlashLoader - fl = FlashLoader(config=config_file) + fl = FlashLoader(config=config_) fl.read_dataframe(runs=[43878, 43879]) start, end = fl.metadata["file_statistics"]["0"]["time_stamps"] @@ -173,10 +205,10 @@ def test_get_elapsed_time_run(config_file): assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 -def test_available_runs(monkeypatch, config_file): +def test_available_runs(monkeypatch, config): """Test available_runs property of FlashLoader class""" # Create an instance of FlashLoader - fl = FlashLoader(config=config_file) + fl = FlashLoader(config=config) # Mock the raw_dir and files fl.raw_dir = "/path/to/raw_dir" diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index 3643feaf..08891320 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -1,8 +1,7 @@ """Tests for utils functionality""" -from pathlib import Path - import pytest +from .test_buffer_handler import create_parquet_dir from sed.loader.flash.utils import get_channels from sed.loader.flash.utils import initialize_paths @@ -79,13 +78,6 @@ def test_get_channels_by_format(config_dataframe): ) -def create_parquet_dir(config, folder): - parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) - parquet_path = parquet_path.joinpath(folder) - parquet_path.mkdir(parents=True, exist_ok=True) - return parquet_path - - def test_parquet_init_error(): """Test ParquetHandler initialization error""" with pytest.raises(ValueError) as e: From b7537a8a19ef17b473feacec4155d39b639b23f8 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 29 Jun 2024 01:10:56 +0200 Subject: [PATCH 125/300] fix dropna --- sed/loader/flash/buffer_handler.py | 4 +++- tests/test_processor.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index e4e6b879..5c392401 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -182,7 +182,9 @@ def _fill_dataframes(self): } # Drop rows with nan values in electron channels - df_electron = dataframe.dropna(subset=get_channels(["per_electron"])) + df_electron = dataframe.dropna( + subset=get_channels(self._config["channels"], ["per_electron"]), + ) # Set the dtypes of the channels here as there should be no null values channel_dtypes = get_channels(self._config["channels"], "all") diff --git a/tests/test_processor.py b/tests/test_processor.py index 1514dd3f..7f634274 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -644,7 +644,7 @@ def test_align_dld_sectors() -> None: assert "dldSectorID" in processor.dataframe.columns sector_delays = np.asarray([10, -10, 20, -20, 30, -30, 40, -40]) - + print(processor.dataframe[processor.dataframe["dldSectorID"] == 0]["dldTimeSteps"].compute()) tof_ref = [] for i in range(len(sector_delays)): tof_ref.append( From b0b090d95d147cffb305f7fb45ca02f6786f2fd9 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 30 Jun 2024 18:06:44 +0200 Subject: [PATCH 126/300] fix minor stuff and add test to see if exception handling works in parallel --- sed/loader/flash/buffer_handler.py | 5 +-- sed/loader/flash/dataframe.py | 4 +-- tests/loader/flash/test_buffer_handler.py | 38 +++++++++++++++++++++++ tests/loader/flash/test_flash_loader.py | 1 - tests/test_processor.py | 1 - 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 5c392401..7c66f8f2 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -140,14 +140,15 @@ def _save_buffer_files(self, debug: bool) -> None: debug (bool): Flag to enable debug mode, which serializes the creation. """ n_cores = min(len(self.missing_h5_files), self.n_cores) + paths = zip(self.missing_h5_files, self.save_paths) if n_cores > 0: if debug: - for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths): + for h5_path, parquet_path in paths: self._save_buffer_file(h5_path, parquet_path) else: Parallel(n_jobs=n_cores, verbose=10)( delayed(self._save_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in zip(self.missing_h5_files, self.save_paths) + for h5_path, parquet_path in paths ) def _fill_dataframes(self): diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 86519740..0067279e 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -39,7 +39,7 @@ def __init__(self, config_dataframe: dict, h5_path: Path) -> None: def get_index_dataset_key(self, channel: str) -> tuple[str, str]: """ - Checks if 'group_name' and converts to 'index_key' and 'dataset_key' if so. + Checks if 'index_key' and 'dataset_key' exists and returns that. Args: channel (str): The name of the channel. @@ -54,7 +54,7 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: if "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] - else: + elif "group_name" in channel_config: print("'group_name' is no longer supported.") raise ValueError( diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 7685d8ec..2df7cf17 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -1,4 +1,5 @@ """Test cases for the BufferHandler class in the Flash module.""" +from copy import deepcopy from pathlib import Path import numpy as np @@ -159,6 +160,43 @@ def test_save_buffer_files(config, h5_paths): [path.unlink() for path in bh_parallel.buffer_paths] +def test_save_buffer_files_exception(config, h5_paths, h5_file_copy, tmp_path): + """Test function to verify exception handling when running code in parallel.""" + folder_parallel = create_parquet_dir(config, "save_buffer_files_exception") + config_ = deepcopy(config) + + # check exception in case of missing channel in config + channel = "dldPosX" + del config_["dataframe"]["channels"][channel]["index_key"] + + # testing exception in parallel execution + with pytest.raises(ValueError): + bh = BufferHandler(config_) + bh.run(h5_paths, folder_parallel, debug=False) + + # check exception message with empty dataset + config_ = deepcopy(config) + channel = "testChannel" + channel_index_key = "test/dataset/empty/index" + empty_dataset_key = "test/dataset/empty/value" + config_["dataframe"]["channels"][channel] = { + "index_key": channel_index_key, + "dataset_key": empty_dataset_key, + "format": "per_train", + } + + # create an empty dataset + h5_file_copy.create_dataset( + name=empty_dataset_key, + shape=0, + ) + + # expect key error because of missing index dataset + with pytest.raises(KeyError): + bh = BufferHandler(config_) + bh.run([tmp_path / "copy.h5"], folder_parallel, debug=False, force_recreate=True) + + def test_get_filled_dataframe(config, h5_paths): """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 4deacded..a6f4a090 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -60,7 +60,6 @@ def test_initialize_dirs( del config_["core"]["beamtime_id"] with pytest.raises(ValueError) as e: fl._initialize_dirs() - print(e.value) assert "The beamtime_id and year are required." in str(e.value) diff --git a/tests/test_processor.py b/tests/test_processor.py index 7f634274..7865afd2 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -644,7 +644,6 @@ def test_align_dld_sectors() -> None: assert "dldSectorID" in processor.dataframe.columns sector_delays = np.asarray([10, -10, 20, -20, 30, -30, 40, -40]) - print(processor.dataframe[processor.dataframe["dldSectorID"] == 0]["dldTimeSteps"].compute()) tof_ref = [] for i in range(len(sector_delays)): tof_ref.append( From 118084e0495d94119bf4619c2f0d3038ba03d0a1 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 30 Jun 2024 18:56:21 +0200 Subject: [PATCH 127/300] fix timestamp issue in flashloader --- sed/loader/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sed/loader/utils.py b/sed/loader/utils.py index f47419c0..2dff23d9 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -252,8 +252,8 @@ def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[st # Get the timestamp min and max try: - timestamps = get_timestamp_stats(file_meta, time_stamp_col) - metadata_dict["time_stamps"] = timestamps + start, end = get_timestamp_stats(file_meta, time_stamp_col) + metadata_dict["time_stamps"] = np.array([start, end]) except ValueError: pass From 2a7673488f98a4f5ee5c01b53e676cb1ff059df4 Mon Sep 17 00:00:00 2001 From: zain-sohail <10053031+zain-sohail@users.noreply.github.com> Date: Sun, 30 Jun 2024 17:02:13 +0000 Subject: [PATCH 128/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index 95d90c3a..9b0160a5 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,7 +1,7 @@ binning_1d: 3.017609174399999 binning_4d: 9.210316116800005 inv_dfield: 5.196141159999996 -loader_compute_flash: 0.048782521849997804 +loader_compute_flash: 0.014761656549995904 loader_compute_mpes: 0.015864623800007395 loader_compute_sxp: 0.006027440450000654 workflow_1d: 17.0553120846 From 7f9422c121f6732a072ee39090a03b13e01c30f1 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 2 Jul 2024 09:40:38 +0200 Subject: [PATCH 129/300] additional spell fixes --- .cspell/custom-dictionary.txt | 96 +++++++++++++------------ sed/loader/flash/dataframe.py | 2 +- tests/loader/flash/test_flash_loader.py | 2 +- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index bf0e50d9..65deda23 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -12,8 +12,11 @@ arraysum ascale astype atleast +autoclass automodule +autoreload autoselect +autosummary axhline axind AXISNAME @@ -30,6 +33,7 @@ binsize binwidth blas bvec +bysource caldir calib calibdict @@ -40,6 +44,8 @@ centroidnn chessy clim cmap +cobertura +codemirror coeffs coldist colgrid @@ -50,6 +56,7 @@ COMPES coordmat coordtype countrate +coverallsapp cryo cstart cstep @@ -60,6 +67,8 @@ cvdist dask datacheck dataconverter +datafile +datafolder dataformat dataframe dataframes @@ -74,10 +83,14 @@ dfpart dfpid dictionarized dictmerge +DLDAUX +dpkg dropna dset dsets dtype +dtypes +easimon ecalibdict electronanalyser endstation @@ -87,12 +100,15 @@ ENERGYDISPERSION ENOSPC equiscale Eref +errorbar evpoly exfel +expanduser Extr faddr Faradayweg fastdtw +fdir featranges ffill figsize @@ -102,10 +118,12 @@ fixedvertex flatidx fluence fontsize +fprocessing ftype fwhm genindex getgid +getmtime gpfs griddata gridplot @@ -113,6 +131,7 @@ groupnames halfbinsize HEXTOF histdd +histdict histkwds histogramdd histtype @@ -134,11 +153,14 @@ ipympl ipynb ipython ipywidgets +isdir isel +isinstance itemsize joblib jpars jupyterlab +kernelspec kmodem KTOF kwds @@ -149,6 +171,7 @@ linalg linekwds linesegkwds linewidth +linspace literalinclude lmfit lmkcenter @@ -159,6 +182,7 @@ macrobunches Maklar mapkwds markersize +matplotlib maxdepth maxs maxsplit @@ -185,14 +209,19 @@ mycopy myfantasymethod myfantasyscale mymakedirs +mypy nanos nans narray natsort natsorted nbagg +nbconvert +nbformat nbin nbins +nbsphinx +nbstripout ncol ncols ndarray @@ -228,8 +257,11 @@ OPCPA openmp OPTICALDELAY otherax +packetcoders Pandoc +parallelly pathcorr +pathlib pbar pcent pcolormesh @@ -251,12 +283,18 @@ prefs printfuncs psutil ptargs +pullrequest pval pyarrow pyenv +pygments pynxtools +pypa +pypi +pyplot pyproject pytest +pyupgrade quantile randn rcond @@ -271,6 +309,7 @@ rowgrid rstart rstep rtol +rtype rvbin rvbins rvname @@ -286,12 +325,17 @@ SDIAG sdir segs sfile +shutil Sixten +sohail specnorm +sphinxext splinewarp stackax stackaxis stepsize +subchannel +subchannels subdir subdirs subfolders @@ -302,7 +346,9 @@ textshift textsize threadpool threadpoolctl +tibdex tifffile +timeit TIMINGINFO Tmain Tmat @@ -314,6 +360,7 @@ traceseg trseg Tsec txtsize +typehints TZCYXS tzoffset ubid @@ -321,6 +368,7 @@ UDLD unbinned uncategorised undoc +utime varnames venv verts @@ -355,51 +403,7 @@ ypos yratio yscale ytrans +zain Zenodo -zraw -typehints -sphinxext -nbsphinx -autoclass -bysource -rtype -nbformat -pygments -nbconvert -codemirror -kernelspec -isinstance -pyplot -matplotlib -pathlib -autoreload -getmtime -utime zfill -expanduser -datafile -autosummary -errorbar -fdir -fprocessing -datafolder -parallelly -histdict -shutil -isdir -linspace -mypy -pyupgrade -nbstripout -timeit -pullrequest -tibdex -packetcoders -easimon -dpkg -zain -sohail -pypi -pypa -cobertura -coverallsapp +zraw diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 0067279e..2af2f2fe 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -249,7 +249,7 @@ def df_train(self) -> pd.DataFrame: (key, [0], [0]), names=self.multi_index, ) - # Auxillary dataset (which is stored in the same dataset as other DLD channels) + # Auxiliary dataset (which is stored in the same dataset as other DLD channels) # contains multiple channels inside. Even though they are resolved per train, # they come in pulse format, so the extra values are sliced and individual channels are # created and appended to the list diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index a6f4a090..dc3c24e4 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -56,7 +56,7 @@ def test_initialize_dirs( assert str(expected_raw_path) == fl.raw_dir assert str(expected_processed_path) == fl.parquet_dir - # remove breamtimeid, year and daq from config to raise error + # remove beamtime_id, year and daq from config to raise error del config_["core"]["beamtime_id"] with pytest.raises(ValueError) as e: fl._initialize_dirs() From b90b7f3e6e49283e1d1a3ffc84427e09f881b2dc Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 27 Jun 2024 19:56:42 +0200 Subject: [PATCH 130/300] change behavior of tof_binning and adc_binning to reflect actual binning factor --- sed/calibrator/delay.py | 7 +++--- sed/calibrator/energy.py | 24 ++++++------------- sed/config/default.yaml | 6 ++--- sed/config/flash_example_config.yaml | 4 ++-- sed/config/mpes_example_config.yaml | 18 +++++++------- sed/core/processor.py | 12 +++------- tests/calibrator/test_energy.py | 8 +++---- .../6_binning_with_time-stamped_data.ipynb | 2 +- tutorial/sxp_config.yaml | 2 +- 9 files changed, 34 insertions(+), 49 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 78dd1e3b..666acbb7 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -144,9 +144,10 @@ def append_delay_axis( t0_key = self._config["delay"].get("t0_key", "") if "adc_range" not in calibration.keys(): - calibration["adc_range"] = np.asarray( - self._config["delay"]["adc_range"], - ) / 2 ** (self._config["dataframe"]["adc_binning"] - 1) + calibration["adc_range"] = ( + np.asarray(self._config["delay"]["adc_range"]) + / self._config["dataframe"]["adc_binning"] + ) if "delay_range" not in calibration.keys(): if "delay_range_mm" not in calibration.keys() or "time0" not in calibration.keys(): diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index b3cbefc8..77bdeccc 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -105,10 +105,8 @@ def __init__( self.binning: int = self._config["dataframe"]["tof_binning"] self.x_width = self._config["energy"]["x_width"] self.y_width = self._config["energy"]["y_width"] - self.tof_width = np.asarray( - self._config["energy"]["tof_width"], - ) / 2 ** (self.binning - 1) - self.tof_fermi = self._config["energy"]["tof_fermi"] / 2 ** (self.binning - 1) + self.tof_width = np.asarray(self._config["energy"]["tof_width"]) / self.binning + self.tof_fermi = self._config["energy"]["tof_fermi"] / self.binning self.color_clip = self._config["energy"]["color_clip"] self.sector_delays = self._config["dataframe"].get("sector_delays", None) self.sector_id_column = self._config["dataframe"].get("sector_id_column", None) @@ -204,9 +202,7 @@ def bin_data( if bins is None: bins = [self._config["energy"]["bins"]] if ranges is None: - ranges_ = [ - np.array(self._config["energy"]["ranges"]) / 2 ** (self.binning - 1), - ] + ranges_ = [np.array(self._config["energy"]["ranges"]) / self.binning] ranges = [cast(tuple[float, float], tuple(v)) for v in ranges_] # pylint: disable=duplicate-code hist_mode = kwds.pop("hist_mode", self._config["binning"]["hist_mode"]) @@ -220,10 +216,7 @@ def bin_data( "threads_per_worker", self._config["binning"]["threads_per_worker"], ) - threadpool_api = kwds.pop( - "threadpool_API", - self._config["binning"]["threadpool_API"], - ) + threadpool_api = kwds.pop("threadpool_API", self._config["binning"]["threadpool_API"]) read_biases = False if biases is None: @@ -2171,10 +2164,7 @@ def residual(pars, time, data, binwidth, binning, energy_scale): name="t0", value=t0_pars.get("value", 1e-6), min=t0_pars.get("min", -np.inf), - max=t0_pars.get( - "max", - (min(pos) - 1) * binwidth * 2**binning, - ), + max=t0_pars.get("max", (min(pos) - 1) * binwidth * binning), vary=t0_pars.get("vary", True), ) E0_pars = kwds.pop("E0", {}) # pylint: disable=invalid-name @@ -2364,7 +2354,7 @@ def tof2ev( # m_e/2 [eV] bin width [s] energy = ( - 2.84281e-12 * sign * (tof_distance / (t * binwidth * 2**binning - time_offset)) ** 2 + 2.84281e-12 * sign * (tof_distance / (t * binwidth * binning - time_offset)) ** 2 + energy_offset ) @@ -2414,5 +2404,5 @@ def tof2ns( Returns: float: Converted time in nanoseconds. """ - val = t * 1e9 * binwidth * 2.0**binning + val = t * 1e9 * binwidth * binning return val diff --git a/sed/config/default.yaml b/sed/config/default.yaml index d0f779be..b047d8a8 100644 --- a/sed/config/default.yaml +++ b/sed/config/default.yaml @@ -31,9 +31,9 @@ dataframe: delay_column: "delay" # time length of a base time-of-flight bin in s tof_binwidth: 4.125e-12 - # Binning factor of the tof_column-data compared to tof_binwidth (2^(tof_binning-1)) + # Binning factor of the tof_column-data compared to tof_binwidth tof_binning: 1 - # binning factor used for the adc coordinate (2^(adc_binning-1)) + # binning factor used for the adc coordinate adc_binning: 1 # list of columns to apply jitter to. jitter_cols: ["@x_column", "@y_column", "@tof_column"] @@ -45,7 +45,7 @@ dataframe: energy: # Number of bins to use for energy calibration traces bins: 1000 - # Bin ranges to use for energy calibration curves (for tof_binning=0) + # Bin ranges to use for energy calibration curves (for tof_binning=1) ranges: [100000, 150000] # Option to normalize energy calibration traces normalize: True diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 7f4cf51b..26f6f337 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -58,8 +58,8 @@ dataframe: time_stamp_alias: timeStamp # time length of a base time-of-flight bin in seconds tof_binwidth: 2.0576131995767355E-11 - # binning parameter for time-of-flight data. 2**tof_binning bins per base bin - tof_binning: 3 # power of 2, 3 means 8 bins per step + # binning parameter for time-of-flight data. + tof_binning: 8 # dataframe column containing sector ID. obtained from dldTimeSteps column sector_id_column: dldSectorID sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 37a2b5ae..c7d08c4c 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -57,10 +57,10 @@ dataframe: delay_column: "delay" # time length of a base time-of-flight bin in ns tof_binwidth: 4.125e-12 - # Binning factor of the tof_column-data compared to tof_binwidth (2^(tof_binning-1)) - tof_binning: 2 - # binning factor used for the adc coordinate (2^(adc_binning-1)) - adc_binning: 3 + # Binning factor of the tof_column-data compared to tof_binwidth + tof_binning: 4 + # binning factor used for the adc coordinate + adc_binning: 4 # Default units for dataframe entries units: X: 'step' @@ -82,8 +82,8 @@ dataframe: energy: # Number of bins to use for energy calibration traces bins: 1000 - # Bin ranges to use for energy calibration curves (for tof_binning=0) - ranges: [128000, 138000] + # Bin ranges to use for energy calibration curves (for tof_binning=1) + ranges: [256000, 276000] # hdf5 path to attribute storing bias information for a given file bias_key: "@KTOF:Lens:Sample:V" # Option to normalize energy calibration traces @@ -102,7 +102,7 @@ energy: energy_scale: "kinetic" # Approximate position of the high-energy-cutoff in tof_column bins, # used for displaying a graph to choose the energy correction function parameters. - tof_fermi: 132250 + tof_fermi: 264500 # TOF range to visualize for the correction tool around tof_fermi tof_width: [-600, 1000] # x-integration range for the correction tool around the center pixel @@ -142,7 +142,7 @@ momentum: # Bin numbers used for the respective axes bins: [512, 512, 300] # bin ranges to use (in unbinned detector coordinates) - ranges: [[-256, 1792], [-256, 1792], [132000, 136000]] + ranges: [[-256, 1792], [-256, 1792], [264000, 272000]] # The x/y pixel ranges of the detector detector_ranges: [[0, 2048], [0, 2048]] # The center pixel of the detector in the binned x/y coordinates @@ -213,7 +213,7 @@ histogram: # Axes names starting with "@" refer to keys in the "dataframe" section axes: ["@x_column", "@y_column", "@tof_column", "@adc_column"] # default ranges to use for histogram visualization (in unbinned detector coordinates) - ranges: [[0, 1800], [0, 1800], [128000, 138000], [0, 32000]] + ranges: [[0, 1800], [0, 1800], [256000, 276000], [0, 32000]] metadata: # URL of the epics archiver request engine diff --git a/sed/core/processor.py b/sed/core/processor.py index b334c1a8..f882de9f 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -2105,9 +2105,7 @@ def pre_binning( bins = self._config["momentum"]["bins"] if ranges is None: ranges_ = list(self._config["momentum"]["ranges"]) - ranges_[2] = np.asarray(ranges_[2]) / 2 ** ( - self._config["dataframe"]["tof_binning"] - 1 - ) + ranges_[2] = np.asarray(ranges_[2]) / self._config["dataframe"]["tof_binning"] ranges = [cast(tuple[float, float], tuple(v)) for v in ranges_] assert self._dataframe is not None, "dataframe needs to be loaded first!" @@ -2407,13 +2405,9 @@ def view_event_histogram( ranges = list(self._config["histogram"]["ranges"]) for loc, axis in enumerate(axes): if axis == self._config["dataframe"]["tof_column"]: - ranges[loc] = np.asarray(ranges[loc]) / 2 ** ( - self._config["dataframe"]["tof_binning"] - 1 - ) + ranges[loc] = np.asarray(ranges[loc]) / self._config["dataframe"]["tof_binning"] elif axis == self._config["dataframe"]["adc_column"]: - ranges[loc] = np.asarray(ranges[loc]) / 2 ** ( - self._config["dataframe"]["adc_binning"] - 1 - ) + ranges[loc] = np.asarray(ranges[loc]) / self._config["dataframe"]["adc_binning"] input_types = map(type, [axes, bins, ranges]) allowed_types = [list, tuple] diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 80c7cbef..378ce095 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -181,7 +181,7 @@ def test_calibrate_append(energy_scale: str, calibration_method: str) -> None: calibration_method (str): method used for calibration """ config = parse_config( - config={"dataframe": {"tof_binning": 2}}, + config={"dataframe": {"tof_binning": 4}}, folder_config={}, user_config={}, system_config={}, @@ -281,7 +281,7 @@ def test_append_tof_ns_axis() -> None: "dataframe": { "tof_column": "t", "tof_ns_column": "t_ns", - "tof_binning": 1, + "tof_binning": 2, "tof_binwidth": 1e-9, }, } @@ -291,7 +291,7 @@ def test_append_tof_ns_axis() -> None: # from kwds df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) ec = EnergyCalibrator(config=config, loader=loader) - df, _ = ec.append_tof_ns_axis(df, binwidth=2e-9, binning=1) + df, _ = ec.append_tof_ns_axis(df, binwidth=2e-9, binning=2) assert config["dataframe"]["tof_ns_column"] in df.columns np.testing.assert_allclose(df[ec.tof_column], df[ec.tof_ns_column] / 4) @@ -303,7 +303,7 @@ def test_append_tof_ns_axis() -> None: np.testing.assert_allclose(df[ec.tof_column], df[ec.tof_ns_column] / 2) -amplitude = 2.5 # pylint: disable=invalid-name +amplitude = 2.5 center = (730, 730) sample = np.array( [ diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index 230386eb..4a103808 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True)" + "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True, verbose=True)" ] }, { diff --git a/tutorial/sxp_config.yaml b/tutorial/sxp_config.yaml index 70e7d163..49134775 100644 --- a/tutorial/sxp_config.yaml +++ b/tutorial/sxp_config.yaml @@ -28,7 +28,7 @@ dataframe: corrected_tof_column: "tm" bias_column: "sampleBias" tof_binwidth: 6.875E-12 # in seconds - tof_binning: 0 + tof_binning: 1 jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] units: From 84421eac229db557a101eb712faa231d7f5ff8bf Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 27 Jun 2024 20:12:49 +0200 Subject: [PATCH 131/300] add additional ignore entries --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 59fa215f..9bcd34f5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ **/buffer/* **/sed_config.yaml **/datasets.json +copy_yaml_to_json.ipynb + +# local copies +**/*.local.* # Byte-compiled / optimized / DLL files __pycache__/ @@ -145,3 +149,6 @@ dmypy.json # IDE stuff \.vscode + +# poetry local config +poetry.toml From 0da2561c74f04d140f744e33c642cffa02e1b1de Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 27 Jun 2024 20:40:21 +0200 Subject: [PATCH 132/300] move num_cores to "core" and use it globally --- sed/binning/binning.py | 2 +- sed/calibrator/energy.py | 2 +- sed/config/flash_example_config.yaml | 14 ++++++++++++-- sed/config/mpes_example_config.yaml | 6 ++---- .../config/sxp_example_config.yaml | 13 ++++++++++++- sed/core/processor.py | 9 +++++---- sed/loader/mirrorutil.py | 11 +++++++---- tutorial/5_sxp_workflow.ipynb | 4 ++-- 8 files changed, 42 insertions(+), 19 deletions(-) rename tutorial/sxp_config.yaml => sed/config/sxp_example_config.yaml (86%) diff --git a/sed/binning/binning.py b/sed/binning/binning.py index 4f904275..8646159f 100644 --- a/sed/binning/binning.py +++ b/sed/binning/binning.py @@ -267,7 +267,7 @@ def bin_dataframe( Defaults to None. pbar (bool, optional): Option to show the tqdm progress bar. Defaults to True. n_cores (int, optional): Number of CPU cores to use for parallelization. - Defaults to all but one of the available cores. Defaults to N_CPU-1. + Defaults to all but one of the available cores. threads_per_worker (int, optional): Limit the number of threads that multiprocessing can spawn. Defaults to 4. threadpool_api (str, optional): The API to use for multiprocessing. diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 77bdeccc..3eefef60 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -209,7 +209,7 @@ def bin_data( mode = kwds.pop("mode", self._config["binning"]["mode"]) pbar = kwds.pop("pbar", self._config["binning"]["pbar"]) try: - num_cores = kwds.pop("num_cores", self._config["binning"]["num_cores"]) + num_cores = kwds.pop("num_cores", self._config["core"]["num_cores"]) except KeyError: num_cores = psutil.cpu_count() - 1 threads_per_worker = kwds.pop( diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 26f6f337..d9ce9184 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -3,6 +3,8 @@ core: # defines the loader loader: flash + # Since this will run on maxwell most probably, we have a lot of cores at our disposal + num_cores: 100 # the beamline where experiment took place beamline: pg2 # the ID number of the beamtime @@ -21,8 +23,16 @@ core: data_parquet_dir: "tests/data/loader/flash/parquet" binning: - # Since this will run on maxwell most probably, we have a lot of cores at our disposal - num_cores: 100 + # Histogram computation mode to use. + hist_mode: "numba" + # Mode for histogram recombination to use + mode: fast + # Whether to display a progress bar + pbar: True + # Number of multithreading threads per worker thread + threads_per_worker: 4 + # API for numpy multithreading + threadpool_API: "blas" dataframe: # The name of the DAQ system to use. Necessary to resolve the filenames/paths. diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index c7d08c4c..5aa99157 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -1,6 +1,8 @@ core: # The loader to use. The mpes loader allows for loading hdf5 files from the METIS momentum microscope. loader: mpes + # Number of parallel threads to use for parallelized jobs (e.g. binning, data conversion, copy, ...) + num_cores: 20 # Option to use the copy tool to mirror data to a local storage location before processing. use_copy_tool: False # path to the root of the source data directory @@ -9,8 +11,6 @@ core: copy_tool_dest: "/path/to/localDataStore/" # optional keywords for the copy tool: copy_tool_kwds: - # number of parallel copy jobs - ntasks: 20 # group id to set for copied files and folders gid: 1001 @@ -199,8 +199,6 @@ binning: mode: "fast" # Whether to display a progress bar pbar: True - # Number of parallel binning threads to use - num_cores: 20 # Number of multithreading threads per worker thread threads_per_worker: 4 # API for numpy multithreading diff --git a/tutorial/sxp_config.yaml b/sed/config/sxp_example_config.yaml similarity index 86% rename from tutorial/sxp_config.yaml rename to sed/config/sxp_example_config.yaml index 49134775..c0757fa5 100644 --- a/tutorial/sxp_config.yaml +++ b/sed/config/sxp_example_config.yaml @@ -1,5 +1,7 @@ core: loader: sxp + # Since this will run on maxwell most probably, we have a lot of cores at our disposal + num_cores: 100 beamtime_id: p005639 year: 202302 beamline: sxp @@ -10,7 +12,16 @@ core: data_parquet_dir: "/path/to/parquet" binning: - num_cores: 10 + # Histogram computation mode to use. + hist_mode: "numba" + # Mode for histogram recombination to use + mode: fast + # Whether to display a progress bar + pbar: True + # Number of multithreading threads per worker thread + threads_per_worker: 4 + # API for numpy multithreading + threadpool_API: "blas" dataframe: ubid_offset: 0 diff --git a/sed/core/processor.py b/sed/core/processor.py index f882de9f..1c1398e2 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -102,10 +102,10 @@ def __init__( for key in config_kwds.keys(): del kwds[key] self._config = parse_config(config, **config_kwds) - num_cores = self._config.get("binning", {}).get("num_cores", N_CPU - 1) + num_cores = self._config["core"].get("num_cores", N_CPU - 1) if num_cores >= N_CPU: num_cores = N_CPU - 1 - self._config["binning"]["num_cores"] = num_cores + self._config["core"]["num_cores"] = num_cores if verbose is None: self.verbose = self._config["core"].get("verbose", False) @@ -154,6 +154,7 @@ def __init__( self.ct = CopyTool( source=self._config["core"]["copy_tool_source"], dest=self._config["core"]["copy_tool_dest"], + num_cores=self._config["core"]["num_cores"], **self._config["core"].get("copy_tool_kwds", {}), ) except KeyError: @@ -2160,7 +2161,7 @@ def compute( - **pbar**: Option to show the tqdm progress bar. Defaults to config["binning"]["pbar"]. - **n_cores**: Number of CPU cores to use for parallelization. - Defaults to config["binning"]["num_cores"] or N_CPU-1. + Defaults to config["core"]["num_cores"] or N_CPU-1. - **threads_per_worker**: Limit the number of threads that multiprocessing can spawn per binning thread. Defaults to config["binning"]["threads_per_worker"]. @@ -2187,7 +2188,7 @@ def compute( hist_mode = kwds.pop("hist_mode", self._config["binning"]["hist_mode"]) mode = kwds.pop("mode", self._config["binning"]["mode"]) pbar = kwds.pop("pbar", self._config["binning"]["pbar"]) - num_cores = kwds.pop("num_cores", self._config["binning"]["num_cores"]) + num_cores = kwds.pop("num_cores", self._config["core"]["num_cores"]) threads_per_worker = kwds.pop( "threads_per_worker", self._config["binning"]["threads_per_worker"], diff --git a/sed/loader/mirrorutil.py b/sed/loader/mirrorutil.py index d6db66e3..03d4a44b 100644 --- a/sed/loader/mirrorutil.py +++ b/sed/loader/mirrorutil.py @@ -13,6 +13,7 @@ from datetime import datetime import dask as d +import psutil from dask.diagnostics import ProgressBar @@ -36,11 +37,13 @@ def __init__( "safetyMargin", 1 * 2**30, ) # Default 500 GB safety margin - self.gid = kwds.pop("gid", 5050) + self.gid = kwds.pop("gid", 1001) self.scheduler = kwds.pop("scheduler", "threads") - # Default to 25 concurrent copy tasks - self.ntasks = int(kwds.pop("ntasks", 25)) + # Default to 20 concurrent copy tasks + self.num_cores = kwds.pop("num_cores", 20) + if self.num_cores >= psutil.cpu_count(): + self.num_cores = psutil.cpu_count() - 1 def copy( self, @@ -162,7 +165,7 @@ def copy( d.compute( *copy_tasks, scheduler=self.scheduler, - num_workers=self.ntasks, + num_workers=self.num_cores, **compute_kwds, ) print("Copy finished!") diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index f7a89220..bb7c6a7b 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -32,8 +32,8 @@ }, "outputs": [], "source": [ - "local_path = Path(sed.__file__).parent.parent / \"tutorial/\"\n", - "config_file = local_path / \"sxp_config.yaml\"\n", + "# pick the default configuration file for the SXP experiment\n", + "config_file = Path('../sed/config/sxp_example_config.yaml')\n", "assert config_file.exists()" ] }, From 232c6cc5712fe5a6cfa37562829931f56fe15237 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 2 Jul 2024 12:20:28 +0200 Subject: [PATCH 133/300] fix spelling --- .cspell/custom-dictionary.txt | 1 + tutorial/4_hextof_workflow.ipynb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 65deda23..dd0d304e 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -76,6 +76,7 @@ datastreams datestring ddir delaxes +delayeds Desy dfield dfops diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 2ee292f0..a2804a40 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -585,7 +585,7 @@ "metadata": {}, "source": [ "### correct offsets\n", - "The energy axis is now correct, taking the sample bias of the measurement into account. Additionally, we can compensate the photon energy (`monocrhomatorPhotonEnergy`) and the `tofVoltage`." + "The energy axis is now correct, taking the sample bias of the measurement into account. Additionally, we can compensate the photon energy (`monochromatorPhotonEnergy`) and the `tofVoltage`." ] }, { From 78b5fbf1114b5ba25bb2e540c27af3e8299cef9b Mon Sep 17 00:00:00 2001 From: "bump[bot]" Date: Tue, 2 Jul 2024 07:26:18 +0000 Subject: [PATCH 134/300] bump version to 0.1.10a6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4c8a7aa8..4e0189d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "sed-processor" packages = [ {include = "sed"} ] -version = "0.1.10a5" +version = "0.1.10a6" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = ["OpenCOMPES team "] readme = "README.md" From cc8145e73ed669af556c4758db335a121f01767d Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 2 Jul 2024 07:28:01 +0000 Subject: [PATCH 135/300] Update dependencies --- poetry.lock | 167 ++++++++++++++++++++++++++++------------------------ 1 file changed, 89 insertions(+), 78 deletions(-) diff --git a/poetry.lock b/poetry.lock index 27df9a11..7e7c8816 100644 --- a/poetry.lock +++ b/poetry.lock @@ -185,17 +185,17 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] [[package]] name = "asteval" -version = "0.9.33" +version = "1.0.0" description = "Safe, minimalistic evaluator of python expression using ast module" optional = false python-versions = ">=3.8" files = [ - {file = "asteval-0.9.33-py3-none-any.whl", hash = "sha256:aae3a0308575a545c8cecc43a6632219e6a90963a56380c74632cf54311e43bf"}, - {file = "asteval-0.9.33.tar.gz", hash = "sha256:94981701f4d252c88aa5e821121b1aabef73a003da138fc6405169c9e675d24d"}, + {file = "asteval-1.0.0-py3-none-any.whl", hash = "sha256:71bf1ebd56da932f8d7decb3fa5cef37080b900e30be7e4647b3233033245d67"}, + {file = "asteval-1.0.0.tar.gz", hash = "sha256:03abb8c4057123e52c9052ff5745ffa0c7022aa73383e6143f2357098a1698d5"}, ] [package.extras] -all = ["Sphinx", "build", "coverage", "pytest", "pytest-cov", "twine"] +all = ["asteval[dev,doc,test]"] dev = ["build", "twine"] doc = ["Sphinx"] test = ["coverage", "pytest", "pytest-cov"] @@ -1309,13 +1309,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.4" +version = "6.29.5" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, - {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, ] [package.dependencies] @@ -3077,84 +3077,95 @@ test = ["pytest-astropy (>=0.10)"] [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] From 7687fd1d376b80921c35c82708173b57b477ac43 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 3 Jul 2024 10:16:01 +0200 Subject: [PATCH 136/300] fix flash tests to also rune fine with defined system config --- tests/data/loader/flash/config.yaml | 3 ++- tests/loader/flash/conftest.py | 6 ++++-- tests/loader/flash/test_flash_loader.py | 25 +++++++++++-------------- tests/loader/test_loaders.py | 3 +++ 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 88c51171..1fcad9b7 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -49,7 +49,8 @@ dataframe: tof_ns_column: dldTime # dataframe column containing corrected time-of-flight data corrected_tof_column: "tm" - + # the time stamp column + time_stamp_alias: timeStamp # time length of a base time-of-flight bin in seconds tof_binwidth: 2.0576131995767355E-11 # binning parameter for time-of-flight data. 2**tof_binning bins per base bin diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 7cec83d8..412fc4d4 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -23,7 +23,7 @@ def fixture_config_file(): Returns: dict: The parsed configuration file. """ - return parse_config(config_path) + return parse_config(config_path, folder_config={}, user_config={}, system_config={}) @pytest.fixture(name="config_dataframe") @@ -33,7 +33,9 @@ def fixture_config_file_dataframe(): Returns: dict: The parsed configuration file. """ - return parse_config(config_path)["dataframe"] + return parse_config(config_path, folder_config={}, user_config={}, system_config={})[ + "dataframe" + ] @pytest.fixture(name="h5_file") diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index dc3c24e4..9442fa00 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -2,7 +2,6 @@ from __future__ import annotations import os -from importlib.util import find_spec from pathlib import Path from typing import Literal @@ -11,10 +10,6 @@ from .test_buffer_handler import create_parquet_dir from sed.loader.flash.loader import FlashLoader -package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") -H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" - @pytest.mark.parametrize( "sub_dir", @@ -79,7 +74,7 @@ def test_initialize_dirs_filenotfound(config: dict) -> None: fl._initialize_dirs() -def test_save_read_parquet_flash(config): +def test_save_read_parquet_flash(config: dict) -> None: """ Test the functionality of saving and reading parquet files with FlashLoader. @@ -133,7 +128,7 @@ def test_save_read_parquet_flash(config): [data_parquet_dir.joinpath(file).unlink() for file in new_files] -def test_get_elapsed_time_fid(config): +def test_get_elapsed_time_fid(config: dict): """Test get_elapsed_time method of FlashLoader class""" # Create an instance of FlashLoader fl = FlashLoader(config=config) @@ -177,13 +172,11 @@ def test_get_elapsed_time_fid(config): assert "Timestamp metadata missing in file 0" in str(e.value) -def test_get_elapsed_time_run(config): - config_ = config.copy() - config_["core"]["paths"] = { - "data_raw_dir": "tests/data/loader/flash/", - "data_parquet_dir": "tests/data/loader/flash/parquet/get_elapsed_time_run", - } +def test_get_elapsed_time_run(config: dict): """Test get_elapsed_time method of FlashLoader class""" + config_ = config.copy() + data_parquet_dir = create_parquet_dir(config_, "get_elapsed_time_run") + config_["core"]["paths"]["data_parquet_dir"] = data_parquet_dir # Create an instance of FlashLoader fl = FlashLoader(config=config_) @@ -203,8 +196,12 @@ def test_get_elapsed_time_run(config): start, end = fl.metadata["file_statistics"]["1"]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 + # remove the parquet files + for file in os.listdir(Path(fl.parquet_dir, "buffer")): + Path(fl.parquet_dir, "buffer").joinpath(file).unlink() + -def test_available_runs(monkeypatch, config): +def test_available_runs(monkeypatch: pytest.MonkeyPatch, config: dict): """Test available_runs property of FlashLoader class""" # Create an instance of FlashLoader fl = FlashLoader(config=config) diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 020e926b..8a5d1507 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -46,6 +46,9 @@ def get_loader_name_from_loader_object(loader: BaseLoader) -> str: loader_name, "config.yaml", ), + folder_config={}, + user_config={}, + system_config={}, ), ) if loader.__name__ is gotten_loader.__name__: From 6e04835659de8f47d8c1d8b5227ce0fb61efa467 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 3 Jul 2024 23:52:07 +0200 Subject: [PATCH 137/300] add typing to tests, and fix issues --- tests/loader/flash/conftest.py | 10 +++--- tests/loader/flash/test_buffer_handler.py | 34 +++++++++++++------- tests/loader/flash/test_dataframe_creator.py | 26 +++++++++------ tests/loader/flash/test_flash_loader.py | 9 +++--- tests/loader/flash/test_flash_metadata.py | 10 +++--- tests/loader/flash/test_utils.py | 10 +++--- 6 files changed, 59 insertions(+), 40 deletions(-) diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 412fc4d4..2bd40b75 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -17,7 +17,7 @@ @pytest.fixture(name="config") -def fixture_config_file(): +def fixture_config_file() -> dict: """Fixture providing a configuration file for FlashLoader tests. Returns: @@ -27,7 +27,7 @@ def fixture_config_file(): @pytest.fixture(name="config_dataframe") -def fixture_config_file_dataframe(): +def fixture_config_file_dataframe() -> dict: """Fixture providing a configuration file for FlashLoader tests. Returns: @@ -39,7 +39,7 @@ def fixture_config_file_dataframe(): @pytest.fixture(name="h5_file") -def fixture_h5_file(): +def fixture_h5_file() -> h5py.File: """Fixture providing an open h5 file. Returns: @@ -49,7 +49,7 @@ def fixture_h5_file(): @pytest.fixture(name="h5_file_copy") -def fixture_h5_file_copy(tmp_path): +def fixture_h5_file_copy(tmp_path: Path) -> h5py.File: """Fixture providing a copy of an open h5 file. Returns: @@ -65,7 +65,7 @@ def fixture_h5_file_copy(tmp_path): @pytest.fixture(name="h5_paths") -def fixture_h5_paths(): +def fixture_h5_paths() -> list[Path]: """Fixture providing a list of h5 file paths. Returns: diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 2df7cf17..719144eb 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -5,12 +5,13 @@ import numpy as np import pandas as pd import pytest +from h5py import File from sed.loader.flash.buffer_handler import BufferHandler from sed.loader.flash.utils import get_channels -def create_parquet_dir(config, folder): +def create_parquet_dir(config: dict, folder: str) -> Path: """ Creates a directory for storing Parquet files based on the provided configuration and folder name. @@ -22,7 +23,7 @@ def create_parquet_dir(config, folder): return parquet_path -def test_get_files_to_read(config, h5_paths): +def test_get_files_to_read(config: dict, h5_paths: list[Path]) -> None: """ Test the BufferHandler's ability to identify files that need to be read and manage buffer file paths. @@ -73,7 +74,7 @@ def test_get_files_to_read(config, h5_paths): assert np.all(bh.save_paths == expected_buffer_paths) -def test_buffer_schema_mismatch(config, h5_paths): +def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: """ Test function to verify schema mismatch handling in the FlashLoader's 'read_dataframe' method. @@ -130,10 +131,11 @@ def test_buffer_schema_mismatch(config, h5_paths): assert "Missing in config: {'gmdTunnel2'}" in expected_error # Clean up created buffer files after the test - [path.unlink() for path in bh.buffer_paths] + for path in bh.buffer_paths: + path.unlink() -def test_save_buffer_files(config, h5_paths): +def test_save_buffer_files(config: dict, h5_paths: list[Path]) -> None: """ Test the BufferHandler's ability to save buffer files serially and in parallel. @@ -156,11 +158,18 @@ def test_save_buffer_files(config, h5_paths): pd.testing.assert_frame_equal(df_serial, df_parallel) # remove buffer files - [path.unlink() for path in bh_serial.buffer_paths] - [path.unlink() for path in bh_parallel.buffer_paths] - - -def test_save_buffer_files_exception(config, h5_paths, h5_file_copy, tmp_path): + for path in bh_serial.buffer_paths: + path.unlink() + for path in bh_parallel.buffer_paths: + path.unlink() + + +def test_save_buffer_files_exception( + config: dict, + h5_paths: list[Path], + h5_file_copy: File, + tmp_path: Path, +) -> None: """Test function to verify exception handling when running code in parallel.""" folder_parallel = create_parquet_dir(config, "save_buffer_files_exception") config_ = deepcopy(config) @@ -197,7 +206,7 @@ def test_save_buffer_files_exception(config, h5_paths, h5_file_copy, tmp_path): bh.run([tmp_path / "copy.h5"], folder_parallel, debug=False, force_recreate=True) -def test_get_filled_dataframe(config, h5_paths): +def test_get_filled_dataframe(config: dict, h5_paths: list[Path]) -> None: """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") bh = BufferHandler(config) @@ -215,4 +224,5 @@ def test_get_filled_dataframe(config, h5_paths): ) assert np.all(list(bh.df_pulse.columns) == channel_pulse) # remove buffer files - [path.unlink() for path in bh.buffer_paths] + for path in bh.buffer_paths: + path.unlink() diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 6125e010..a84b417f 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -1,4 +1,6 @@ """Tests for DataFrameCreator functionality""" +from pathlib import Path + import h5py import numpy as np import pytest @@ -10,7 +12,7 @@ from sed.loader.flash.utils import get_channels -def test_get_index_dataset_key(config_dataframe, h5_paths): +def test_get_index_dataset_key(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of the index and dataset keys for a given channel.""" config = config_dataframe channel = "dldPosX" @@ -25,7 +27,7 @@ def test_get_index_dataset_key(config_dataframe, h5_paths): df.get_index_dataset_key(channel) -def test_get_dataset_array(config_dataframe, h5_paths): +def test_get_dataset_array(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of a h5py dataset for a given channel.""" df = DataFrameCreator(config_dataframe, h5_paths[0]) @@ -50,7 +52,11 @@ def test_get_dataset_array(config_dataframe, h5_paths): assert dset.shape[1] == 500 -def test_empty_get_dataset_array(config_dataframe, h5_paths, h5_file_copy): +def test_empty_get_dataset_array( + config_dataframe: dict, + h5_paths: list[Path], + h5_file_copy: h5py.File, +) -> None: """Test the method when given an empty dataset.""" channel = "gmdTunnel" @@ -78,7 +84,7 @@ def test_empty_get_dataset_array(config_dataframe, h5_paths, h5_file_copy): assert dset_empty.shape[1] == 0 -def test_pulse_index(config_dataframe, h5_paths): +def test_pulse_index(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of the pulse index for electron resolved data""" df = DataFrameCreator(config_dataframe, h5_paths[0]) @@ -113,7 +119,7 @@ def test_pulse_index(config_dataframe, h5_paths): assert index.is_monotonic_increasing -def test_df_electron(config_dataframe, h5_paths): +def test_df_electron(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of a pandas DataFrame for a channel of type [per electron].""" df = DataFrameCreator(config_dataframe, h5_paths[0]) @@ -152,7 +158,7 @@ def test_df_electron(config_dataframe, h5_paths): ) -def test_create_dataframe_per_pulse(config_dataframe, h5_paths): +def test_create_dataframe_per_pulse(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of a pandas DataFrame for a channel of type [per pulse].""" df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df_pulse @@ -182,7 +188,7 @@ def test_create_dataframe_per_pulse(config_dataframe, h5_paths): ) -def test_create_dataframe_per_train(config_dataframe, h5_paths): +def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of a pandas DataFrame for a channel of type [per train].""" df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df_train @@ -235,7 +241,7 @@ def test_create_dataframe_per_train(config_dataframe, h5_paths): assert result_df.index.is_unique -def test_group_name_not_in_h5(config_dataframe, h5_paths): +def test_group_name_not_in_h5(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test ValueError when the group_name for a channel does not exist in the H5 file.""" channel = "dldPosX" config = config_dataframe @@ -246,7 +252,7 @@ def test_group_name_not_in_h5(config_dataframe, h5_paths): df.df_electron -def test_create_dataframe_per_file(config_dataframe, h5_paths): +def test_create_dataframe_per_file(config_dataframe: dict, h5_paths: list[Path]) -> None: """Test the creation of pandas DataFrames for a given file.""" df = DataFrameCreator(config_dataframe, h5_paths[0]) result_df = df.df @@ -258,7 +264,7 @@ def test_create_dataframe_per_file(config_dataframe, h5_paths): assert result_df.shape[0] == len(all_keys.unique()) -def test_get_index_dataset_key_error(config_dataframe, h5_paths): +def test_get_index_dataset_key_error(config_dataframe: dict, h5_paths: list[Path]) -> None: """ Test that a ValueError is raised when the dataset_key is missing for a channel in the config. """ diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 9442fa00..4fec8c8c 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -125,10 +125,11 @@ def test_save_read_parquet_flash(config: dict) -> None: assert new_files != final_files, "Files were not overwritten after the third call." # remove the parquet files - [data_parquet_dir.joinpath(file).unlink() for file in new_files] + for file in new_files: + data_parquet_dir.joinpath(file).unlink() -def test_get_elapsed_time_fid(config: dict): +def test_get_elapsed_time_fid(config: dict) -> None: """Test get_elapsed_time method of FlashLoader class""" # Create an instance of FlashLoader fl = FlashLoader(config=config) @@ -172,7 +173,7 @@ def test_get_elapsed_time_fid(config: dict): assert "Timestamp metadata missing in file 0" in str(e.value) -def test_get_elapsed_time_run(config: dict): +def test_get_elapsed_time_run(config: dict) -> None: """Test get_elapsed_time method of FlashLoader class""" config_ = config.copy() data_parquet_dir = create_parquet_dir(config_, "get_elapsed_time_run") @@ -201,7 +202,7 @@ def test_get_elapsed_time_run(config: dict): Path(fl.parquet_dir, "buffer").joinpath(file).unlink() -def test_available_runs(monkeypatch: pytest.MonkeyPatch, config: dict): +def test_available_runs(monkeypatch: pytest.MonkeyPatch, config: dict) -> None: """Test available_runs property of FlashLoader class""" # Create an instance of FlashLoader fl = FlashLoader(config=config) diff --git a/tests/loader/flash/test_flash_metadata.py b/tests/loader/flash/test_flash_metadata.py index af376d0d..5b30b0da 100644 --- a/tests/loader/flash/test_flash_metadata.py +++ b/tests/loader/flash/test_flash_metadata.py @@ -7,14 +7,14 @@ @pytest.fixture -def mock_requests(requests_mock): +def mock_requests(requests_mock) -> None: # Mocking the response for the dataset URL dataset_url = "https://example.com/Datasets/11013410%2F43878" requests_mock.get(dataset_url, json={"fake": "data"}, status_code=200) # Test cases for MetadataRetriever -def test_get_metadata(mock_requests): # noqa: ARG001 +def test_get_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "scicat_url": "https://example.com", "scicat_token": "fake_token", @@ -25,7 +25,7 @@ def test_get_metadata(mock_requests): # noqa: ARG001 assert metadata == {"fake": "data"} -def test_get_metadata_with_existing_metadata(mock_requests): # noqa: ARG001 +def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "scicat_url": "https://example.com", "scicat_token": "fake_token", @@ -37,7 +37,7 @@ def test_get_metadata_with_existing_metadata(mock_requests): # noqa: ARG001 assert metadata == {"existing": "metadata", "fake": "data"} -def test_get_metadata_per_run(mock_requests): # noqa: ARG001 +def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "scicat_url": "https://example.com", "scicat_token": "fake_token", @@ -48,7 +48,7 @@ def test_get_metadata_per_run(mock_requests): # noqa: ARG001 assert metadata == {"fake": "data"} -def test_create_dataset_url_by_PID(): +def test_create_dataset_url_by_PID() -> None: metadata_config = { "scicat_url": "https://example.com", "scicat_token": "fake_token", diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index 08891320..4fc3fee6 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -1,4 +1,6 @@ """Tests for utils functionality""" +from pathlib import Path + import pytest from .test_buffer_handler import create_parquet_dir @@ -23,7 +25,7 @@ INDEX_CHANNELS = ["trainId", "pulseId", "electronId"] -def test_get_channels_by_format(config_dataframe): +def test_get_channels_by_format(config_dataframe: dict) -> None: """ Test function to verify the 'get_channels' method in FlashLoader class for retrieving channels based on formats and index inclusion. @@ -78,7 +80,7 @@ def test_get_channels_by_format(config_dataframe): ) -def test_parquet_init_error(): +def test_parquet_init_error() -> None: """Test ParquetHandler initialization error""" with pytest.raises(ValueError) as e: _ = initialize_paths(filenames="test") @@ -86,12 +88,12 @@ def test_parquet_init_error(): assert "Please provide folder or paths." in str(e.value) with pytest.raises(ValueError) as e: - _ = initialize_paths(folder="test") + _ = initialize_paths(folder=Path("test")) assert "With folder, please provide filenames." in str(e.value) -def test_initialize_paths(config): +def test_initialize_paths(config: dict) -> None: """Test ParquetHandler initialization""" folder = create_parquet_dir(config, "parquet_init") From 6b5ff8a840bb5b6eb0e4abde06617b5f2d4d3cd0 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 3 Jul 2024 09:25:36 +0200 Subject: [PATCH 138/300] detect illegal keyword arguments --- sed/binning/binning.py | 8 +-- sed/calibrator/energy.py | 59 +++++++++++--------- sed/calibrator/momentum.py | 92 +++++++++++++++++++++---------- sed/core/dfops.py | 7 ++- sed/core/processor.py | 33 +++++++---- sed/diagnostics.py | 11 +++- sed/loader/flash/loader.py | 10 +++- sed/loader/mirrorutil.py | 9 +++ sed/loader/mpes/loader.py | 29 ++++++---- sed/loader/sxp/loader.py | 6 ++ tests/calibrator/test_momentum.py | 4 ++ 11 files changed, 184 insertions(+), 84 deletions(-) diff --git a/sed/binning/binning.py b/sed/binning/binning.py index 4f904275..a8e4c70c 100644 --- a/sed/binning/binning.py +++ b/sed/binning/binning.py @@ -207,7 +207,7 @@ def bin_dataframe( threads_per_worker: int = 4, threadpool_api: str = "blas", return_partitions: bool = False, - **kwds, + compute_kwds: dict = {}, ) -> xr.DataArray: """Computes the n-dimensional histogram on columns of a dataframe, parallelized. @@ -275,7 +275,7 @@ def bin_dataframe( return_partitions (bool, optional): Option to return a hypercube of dimension n+1, where the last dimension corresponds to the dataframe partitions. Defaults to False. - **kwds: Keyword arguments passed to ``dask.compute()`` + compute_kwds (dict, optional): Dict of Keyword arguments passed to ``dask.compute()`` Raises: Warning: Warns if there are unimplemented features the user is trying to use. @@ -335,7 +335,7 @@ def bin_dataframe( ) if len(core_tasks) > 0: - core_results = dask.compute(*core_tasks, **kwds) + core_results = dask.compute(*core_tasks, **compute_kwds) if return_partitions: for core_result in core_results: @@ -376,7 +376,7 @@ def bin_dataframe( combine_tasks.append( dask.delayed(reduce)(_arraysum, combine_parts), ) - combine_results = dask.compute(*combine_tasks, **kwds) + combine_results = dask.compute(*combine_tasks, **compute_kwds) # Directly fill into target array. This is much faster than # the (not so parallel) reduce/concatenation used before, # and uses less memory. diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 97d18cb6..5b51e834 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -307,8 +307,10 @@ def adjust_ranges( Defaults to 7. apply (bool, optional): Option to directly apply the provided parameters. Defaults to False. - **kwds: - keyword arguments for trace alignment (see ``find_correspondence()``). + **kwds: keyword arguments + - *labels*: List of labels for plotting. Default uses the bias voltages. + - *figsize* Figure size. + Additional keyword arguments are passed to ``find_correspondence()``. """ if traces is None: traces = self.traces_normed @@ -317,8 +319,7 @@ def adjust_ranges( ranges=ranges, ref_id=ref_id, traces=traces, - infer_others=True, - mode="replace", + **kwds, ) self.feature_extract(peak_window=peak_window) @@ -332,25 +333,12 @@ def adjust_ranges( for itr, color in zip(range(len(traces)), colors): trace = traces[itr, :] # main traces - ax.plot( - self.tof, - trace, - ls="-", - color=color, - linewidth=1, - label=labels[itr], - ) + ax.plot(self.tof, trace, ls="-", color=color, linewidth=1, label=labels[itr]) # segments: seg = self.featranges[itr] cond = (self.tof >= seg[0]) & (self.tof <= seg[1]) tofseg, traceseg = self.tof[cond], trace[cond] - (line,) = ax.plot( - tofseg, - traceseg, - ls="-", - color=color, - linewidth=3, - ) + (line,) = ax.plot(tofseg, traceseg, ls="-", color=color, linewidth=3) plot_segs.append(line) # markers (scatt,) = ax.plot( @@ -366,7 +354,7 @@ def adjust_ranges( ax.set_title("") def update(refid, ranges): - self.add_ranges(ranges, refid, traces=traces) + self.add_ranges(ranges, refid, traces=traces, **kwds) self.feature_extract(peak_window=7) for itr, _ in enumerate(self.traces_normed): seg = self.featranges[itr] @@ -410,6 +398,7 @@ def apply_func(apply: bool): # noqa: ARG001 ranges_slider.value, refid_slider.value, traces=self.traces_normed, + **kwds, ) self.feature_extract(peak_window=7) ranges_slider.close() @@ -925,6 +914,10 @@ def append_tof_ns_axis( Defaults to config["energy"]["tof_binwidth"]. binning (int, optional): Time-of-flight binning factor. Defaults to config["energy"]["tof_binning"]. + **kwds: + + - **binwidth**: Binwidth in ns. + - **binning**: Binning factor of the TOF column. Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with the new columns @@ -932,6 +925,10 @@ def append_tof_ns_axis( """ binwidth = kwds.pop("binwidth", self.binwidth) binning = kwds.pop("binning", self.binning) + + if len(kwds) > 0: + raise TypeError(f"append_tof_ns_axis() got unexpected keyword arguments {kwds.keys()}.") + if tof_column is None: if self.corrected_tof_column in df.columns: tof_column = self.corrected_tof_column @@ -1853,6 +1850,8 @@ def find_correspondence( sig_still (np.ndarray): Reference 1D signals. sig_mov (np.ndarray): 1D signal to be aligned. **kwds: keyword arguments for ``fastdtw.fastdtw()`` + - *dist_metric*: Distance metric to use for time warping. Defaults to None. + - *radius*: Radius to use for time warping. Defaults to 1. Returns: np.ndarray: Pixel-wise path correspondences between two input 1D arrays @@ -1860,6 +1859,10 @@ def find_correspondence( """ dist = kwds.pop("dist_metric", None) rad = kwds.pop("radius", 1) + + if len(kwds) > 0: + raise TypeError(f"find_correspondence() got unexpected keyword arguments {kwds.keys()}.") + _, pathcorr = fastdtw(sig_still, sig_mov, dist=dist, radius=rad) return np.asarray(pathcorr) @@ -2168,8 +2171,14 @@ def residual(pars, time, data, binwidth, binning, energy_scale): return model return model - data - pars = Parameters() d_pars = kwds.pop("d", {}) + t0_pars = kwds.pop("t0", {}) + E0_pars = kwds.pop("E0", {}) + + if len(kwds) > 0: + raise TypeError(f"fit_energy_calibration() got unexpected keyword arguments {kwds.keys()}.") + + pars = Parameters() pars.add( name="d", value=d_pars.get("value", 1), @@ -2177,7 +2186,6 @@ def residual(pars, time, data, binwidth, binning, energy_scale): max=d_pars.get("max", np.inf), vary=d_pars.get("vary", True), ) - t0_pars = kwds.pop("t0", {}) pars.add( name="t0", value=t0_pars.get("value", 1e-6), @@ -2188,7 +2196,6 @@ def residual(pars, time, data, binwidth, binning, energy_scale): ), vary=t0_pars.get("vary", True), ) - E0_pars = kwds.pop("E0", {}) # pylint: disable=invalid-name pars.add( name="E0", value=E0_pars.get("value", min(vals)), @@ -2263,8 +2270,8 @@ def poly_energy_calibration( (1=no change, 2=double, etc). Defaults to 1. method (str, optional): Method for determining the energy calibration. - - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form. - - **'lstsq'**, **'lsqr'**: Energy calibration using polynomial form.. + - **'lstsq'**: Use ``numpy.linalg.lstsq``. + - **'lsqr'**: Use ``scipy.sparse.linalg.lsqr`` Defaults to "lstsq". energy_scale (str, optional): Direction of increasing energy scale. @@ -2272,6 +2279,8 @@ def poly_energy_calibration( - **'kinetic'**: increasing energy with decreasing TOF. - **'binding'**: increasing energy with increasing TOF. + **kwds: Keyword arguments passed to ``lsqr``. + Returns: dict: A dictionary of fitting parameters including the following, diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 7cb16bb3..da70c55a 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -317,7 +317,7 @@ def add_features( direction: str = "ccw", rotsym: int = 6, symscores: bool = True, - **kwds, + symtype: str = "rotation", ): """Add features as reference points provided as np.ndarray. If provided, detects the center of the points and orders the points. @@ -331,9 +331,7 @@ def add_features( Direction for ordering the points. Defaults to "ccw". symscores (bool, optional): Option to calculate symmetry scores. Defaults to False. - **kwds: Keyword arguments. - - - **symtype** (str): Type of symmetry scores to calculate + symtype (str, optional): Type of symmetry scores to calculate if symscores is True. Defaults to "rotation". Raises: @@ -374,7 +372,6 @@ def add_features( self.calc_geometric_distances() if symscores is True: - symtype = kwds.pop("symtype", "rotation") self.csm_original = self.calc_symmetry_scores(symtype=symtype) if self.rotsym == 6 and self.pcent is not None: @@ -407,7 +404,8 @@ def feature_extract( symscores (bool, optional): Option for calculating symmetry scores. Defaults to True. **kwds: - Extra keyword arguments for ``symmetrize.pointops.peakdetect2d()``. + Extra keyword arguments for ``symmetrize.pointops.peakdetect2d()`` and + ``add_features()``. Raises: NotImplementedError: @@ -419,6 +417,15 @@ def feature_extract( else: raise ValueError("No image loaded for feature extraction!") + # split off config keywords + feature_kwds = { + key: value + for key, value in kwds.items() + if key in self.add_features.__code__.co_varnames + } + for key in feature_kwds.keys(): + del kwds[key] + if feature_type == "points": # Detect the point landmarks self.peaks = po.peakdetect2d(image, **kwds) @@ -428,7 +435,7 @@ def feature_extract( direction=direction, rotsym=rotsym, symscores=symscores, - **kwds, + **feature_kwds, ) else: raise NotImplementedError @@ -461,7 +468,7 @@ def feature_select( apply (bool, optional): Option to directly store the features in the class. Defaults to False. **kwds: - Extra keyword arguments for ``symmetrize.pointops.peakdetect2d()``. + Keyword arguments for ``add_features``. Raises: ValueError: If no valid image is found from which to ge the coordinates. @@ -540,11 +547,7 @@ def apply_func(apply: bool): # noqa: ARG001 fig.canvas.draw_idle() - self.add_features( - features=features, - rotsym=rotsym, - **kwds, - ) + self.add_features(features=features, rotsym=rotsym, **kwds) apply_button = ipw.Button(description="apply") display(apply_button) @@ -627,6 +630,9 @@ def spline_warp_estimate( - **new_centers**: (dict): User-specified center positions for the reference and target sets. {'lmkcenter': (row, col), 'targcenter': (row, col)} + + Additional keywords are passed to ``tpsWarping()``. + Returns: np.ndarray: The corrected image. """ @@ -676,7 +682,7 @@ def spline_warp_estimate( "No valid landmarks defined, and no landmarks found in configuration!", ) from exc - self.add_features(features=features, rotsym=rotsym, include_center=include_center) + self.add_features(features=features, rotsym=rotsym) else: self.correction["creation_date"] = datetime.now().timestamp() @@ -711,6 +717,7 @@ def spline_warp_estimate( self.prefs = kwds.pop("landmarks", self.pouter_ord) self.ptargs = kwds.pop("targets", []) + newcenters = kwds.pop("new_centers", {}) # Generate the target point set if not self.ptargs: @@ -732,7 +739,6 @@ def spline_warp_estimate( self.ptargs = np.column_stack((self.ptargs.T, self.pcent)).T else: # Add different centers to the reference and target sets - newcenters = kwds.pop("new_centers", {}) self.prefs = np.column_stack( (self.prefs.T, newcenters["lmkcenter"]), ).T @@ -829,6 +835,10 @@ def reset_deformation(self, **kwds): """ image = kwds.pop("image", self.slice) coordtype = kwds.pop("coordtype", "cartesian") + + if len(kwds) > 0: + raise TypeError(f"reset_deformation() got unexpected keyword arguments {kwds.keys()}.") + coordmat = sym.coordinate_matrix_2D( image, coordtype=coordtype, @@ -892,7 +902,12 @@ def coordinate_transform( mapkwds (dict, optional): Additional arguments passed to ``scipy.ndimage.map_coordinates()``. Defaults to None. **kwds: keyword arguments. - Additional arguments in specific deformation field. + + - **image**: Image to use. Defaults to self.slice. + - **stackaxis**: Stacking axis for coordinate transformation matrices. + Defaults to 0. + + Additional arguments are passed to the specific deformation field generators. See ``symmetrize.sym`` module. Returns: np.ndarray: The corrected image. @@ -1081,8 +1096,14 @@ def pose_adjustment( transformations = deepcopy(self.transformations) if len(kwds) > 0: - for key, value in kwds.items(): - transformations[key] = value + for key in ["scale", "xtrans", "ytrans", "angle"]: + if key in kwds: + transformations[key] = kwds.pop(key) + + if len(kwds) > 0: + raise TypeError( + f"pose_adjustment() got unexpected keyword arguments {kwds.keys()}.", + ) elif "creation_date" in transformations and verbose: datestring = datetime.fromtimestamp(transformations["creation_date"]).strftime( @@ -1253,7 +1274,7 @@ def calc_inverse_dfield(self): return self.inverse_dfield - def view( # pylint: disable=dangerous-default-value + def view( self, image: np.ndarray = None, origin: str = "lower", @@ -1312,6 +1333,13 @@ def view( # pylint: disable=dangerous-default-value tsr, tsc = kwds.pop("textshift", (3, 3)) txtsize = kwds.pop("textsize", 12) + # Handle unexpected kwds: + handled_kwds = {"figsize"} + if not set(kwds.keys()).issubset(handled_kwds): + raise TypeError( + f"view() got unexpected keyword arguments {set(kwds.keys()) - handled_kwds}.", + ) + if backend == "matplotlib": fig_plt, ax = plt.subplots(figsize=figsize) ax.imshow(image.T, origin=origin, cmap=cmap, **imkwds) @@ -1678,7 +1706,6 @@ def apply_corrections( new_x_column: str = None, new_y_column: str = None, verbose: bool = True, - **kwds, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and replace the X and Y values with their distortion-corrected version. @@ -1698,13 +1725,6 @@ def apply_corrections( Defaults to config["momentum"]["corrected_y_column"]. verbose (bool, optional): Option to report the used landmarks for correction. Defaults to True. - **kwds: Keyword arguments: - - - **dfield**: Inverse dfield - - **cdeform_field**, **rdeform_field**: Column- and row-wise forward - deformation fields. - - Additional keyword arguments are passed to ``apply_dfield``. Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with @@ -1748,7 +1768,6 @@ def apply_corrections( new_x_column=new_x_column, new_y_column=new_y_column, detector_ranges=self.detector_ranges, - **kwds, ) metadata = self.gather_correction_metadata() @@ -1885,10 +1904,23 @@ def append_k_axis( calibration = deepcopy(self.calibration) if len(kwds) > 0: - for key, value in kwds.items(): - calibration[key] = value + for key in [ + "rstart", + "cstart", + "x_center", + "y_center", + "kx_scale", + "ky_scale", + "rstep", + "cstep", + ]: + if key in kwds: + calibration[key] = kwds.pop(key) calibration["creation_date"] = datetime.now().timestamp() + if len(kwds) > 0: + raise TypeError(f"append_k_axis() got unexpected keyword arguments {kwds.keys()}.") + try: (df[new_x_column], df[new_y_column]) = detector_coordinates_2_k_coordinates( r_det=df[x_column], diff --git a/sed/core/dfops.py b/sed/core/dfops.py index d29bf2e0..130d602c 100644 --- a/sed/core/dfops.py +++ b/sed/core/dfops.py @@ -138,6 +138,7 @@ def add_time_stamped_data( data (np.ndarray): Values corresponding at the time stamps in time_stamps dest_column (str): destination column name time_stamp_column (str): Time stamp column name + **kwds: Keyword arguments passed to map_partitions Returns: dask.dataframe.DataFrame: Dataframe with added column @@ -177,7 +178,11 @@ def map_columns_2d( map_2d (Callable): 2D mapping function. x_column (str): The X column of the dataframe to apply mapping to. y_column (str): The Y column of the dataframe to apply mapping to. - **kwds: Additional arguments for the 2D mapping function. + **kwds: + - *new_x_column": Name of the new x-column. Default is to overwrite the x-column. + - *new_y_column": Name of the new y-column. Default is to overwrite the y-column. + + Additional keyword argument are passed to the 2D mapping function. Returns: pd.DataFrame | dask.dataframe.DataFrame: Dataframe with mapped columns. diff --git a/sed/core/processor.py b/sed/core/processor.py index 9d22eb04..79cb86f4 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -96,6 +96,7 @@ def __init__( Defaults to config["core"]["verbose"] or False. **kwds: Keyword arguments passed to parse_config and to the reader. """ + # split off config keywords config_kwds = { key: value for key, value in kwds.items() if key in parse_config.__code__.co_varnames } @@ -275,6 +276,7 @@ def add_attribute(self, attributes: dict, name: str, **kwds): Args: attributes (dict): The attributes dictionary object to add. name (str): Key under which to add the dictionary to the attributes. + **kwds: Additional keywords are passed to the ``MetaHandler.add()`` function. """ self._attributes.add( entry=attributes, @@ -385,7 +387,10 @@ def load( folder (str, optional): Folder path to pass to the loader. Defaults to None. collect_metadata (bool, optional): Option for collecting metadata in the reader. - **kwds: Keyword parameters passed to the reader. + **kwds: + - *timed_dataframe*: timed dataframe if dataframe is provided. + + Additional keyword parameters are passed to ``loader.read_dataframe()``. Raises: ValueError: Raised if no valid input is provided. @@ -931,7 +936,7 @@ def apply_momentum_calibration( Defaults to False. verbose (bool, optional): Option to print out diagnostic information. Defaults to config["core"]["verbose"]. - **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``. + **kwds: Keyword args passed to ``MomentumCalibrator.append_k_axis``. """ if verbose is None: verbose = self.verbose @@ -1601,7 +1606,7 @@ def append_tof_ns_axis( Defaults to False. verbose (bool, optional): Option to print out diagnostic information. Defaults to config["core"]["verbose"]. - **kwds: additional arguments are passed to ``EnergyCalibrator.tof_step_to_ns()``. + **kwds: additional arguments are passed to ``EnergyCalibrator.append_tof_ns_axis()``. """ if verbose is None: @@ -1734,12 +1739,10 @@ def calibrate_delay_axis( if len(self.dc.calibration) == 0: try: datafile = self._files[0] - except IndexError: - print( - "No datafile available, specify either", - " 'datafile' or 'delay_range'", - ) - raise + except IndexError as exc: + raise IndexError( + "No datafile available, specify either 'datafile' or 'delay_range'", + ) from exc df, metadata = self.dc.append_delay_axis( self._dataframe, @@ -2023,7 +2026,11 @@ def add_time_stamped_data( If omitted, data are retrieved from the epics archiver. archiver_channel (str, optional): EPICS archiver channel from which to retrieve data. Either this or data and time_stamps have to be present. - **kwds: additional keyword arguments passed to ``add_time_stamped_data``. + **kwds: + + - **time_stamp_column**: Dataframe column containing time-stamp data + + Additional keyword arguments passed to ``add_time_stamped_data``. """ time_stamp_column = kwds.pop( "time_stamp_column", @@ -2322,6 +2329,12 @@ def get_normalization_histogram( raise ValueError(f"Axis '{axis}' not found in binned data!") df_partitions: int | Sequence[int] = kwds.pop("df_partitions", None) + + if len(kwds) > 0: + raise TypeError( + f"get_normalization_histogram() got unexpected keyword arguments {kwds.keys()}.", + ) + if isinstance(df_partitions, int): df_partitions = list(range(0, min(df_partitions, self._dataframe.npartitions))) if use_time_stamps or self._timed_dataframe is None: diff --git a/sed/diagnostics.py b/sed/diagnostics.py index feeeef58..02bcc0a0 100644 --- a/sed/diagnostics.py +++ b/sed/diagnostics.py @@ -24,7 +24,10 @@ def plot_single_hist( histvals (np.ndarray): Histogram counts (e.g. vertical axis). edges (np.ndarray): Histogram edge values (e.g. horizontal axis). legend (str, optional): Text for the plot legend. Defaults to None. - **kwds: Keyword arguments for ``bokeh.plotting.figure().quad()``. + **kwds: + - *tooltip*: Tooltip formatting tuple. Defaults to [("(x, y)", "($x, $y)")] + + Additional keyword arguments are passed to ``bokeh.plotting.figure().quad()``. Returns: pbk.figure: An instance of 'bokeh.plotting.figure' as a plot handle. @@ -77,7 +80,8 @@ def grid_histogram( histkwds (dict, optional): Keyword arguments for histogram plots. Defaults to None. legkwds (dict, optional): Keyword arguments for legends. Defaults to None. - **kwds: Additional keyword arguments. + **kwds: + - *figsize*: Figure size. Defaults to (14, 8) """ if histkwds is None: histkwds = {} @@ -86,6 +90,9 @@ def grid_histogram( figsz = kwds.pop("figsize", (14, 8)) + if len(kwds) > 0: + raise TypeError(f"grid_histogram() got unexpected keyword arguments {kwds.keys()}.") + if backend == "matplotlib": nrv = len(rvs) nrow = int(np.ceil(nrv / ncol)) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 01d2aa62..40123eac 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -243,7 +243,12 @@ def get_elapsed_time_from_run(run_id): return sum(get_elapsed_time_from_fid(fid) for fid in fids) elapsed_times = [] - runs = kwds.get("runs") + runs = kwds.pop("runs", None) + aggregate = kwds.pop("aggregate", True) + + if len(kwds) > 0: + raise TypeError(f"get_elapsed_time() got unexpected keyword arguments {kwds.keys()}.") + if runs is not None: elapsed_times = [get_elapsed_time_from_run(run) for run in runs] else: @@ -251,7 +256,7 @@ def get_elapsed_time_from_run(run_id): fids = range(len(self.files)) elapsed_times = [get_elapsed_time_from_fid(fid) for fid in fids] - if kwds.get("aggregate", True): + if aggregate: elapsed_times = sum(elapsed_times) return elapsed_times @@ -284,6 +289,7 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. + **kwds: Additional keyword arguments passed to ``parse_metadata``. Returns: tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame diff --git a/sed/loader/mirrorutil.py b/sed/loader/mirrorutil.py index d6db66e3..01c1bef2 100644 --- a/sed/loader/mirrorutil.py +++ b/sed/loader/mirrorutil.py @@ -22,6 +22,11 @@ class CopyTool: Args: source (str): Source path for the copy tool. dest (str): Destination path for the copy tool. + **kwds: + - *safetyMargin*: Size in Byte to keep free. Defaults to 500 GBytes. + - *gid*: Group id to which file ownership will be set. Defaults to 1001. + - *scheduler*: Dask scheduler to use. Defaults to "threads". + - *ntasks*: number of cores to use for copying. Defaults to 25. """ def __init__( @@ -42,6 +47,9 @@ def __init__( # Default to 25 concurrent copy tasks self.ntasks = int(kwds.pop("ntasks", 25)) + if len(kwds) > 0: + raise TypeError(f"CopyTool() got unexpected keyword arguments {kwds.keys()}.") + def copy( self, source: str, @@ -53,6 +61,7 @@ def copy( Args: source (str): source path force_copy (bool, optional): re-copy all files. Defaults to False. + **compute_kwds: Keyword arguments passed to dask.compute() Raises: FileNotFoundError: Raised if the source path is not found or empty. diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c24f94c8..821f716a 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -33,7 +33,7 @@ def hdf5_to_dataframe( time_stamp_alias: str = "timeStamps", ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", - **kwds, + test_fid: int = 0, ) -> ddf.DataFrame: """Function to read a selection of hdf5-files, and generate a delayed dask dataframe from provided groups in the files. Optionally, aliases can be defined. @@ -51,12 +51,12 @@ def hdf5_to_dataframe( Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". + test_fid(int, optional): File ID to use for extracting shape information. Returns: ddf.DataFrame: The delayed Dask DataFrame """ # Read a file to parse the file structure - test_fid = kwds.pop("test_fid", 0) test_proc = h5py.File(files[test_fid]) if channels is None: @@ -135,7 +135,7 @@ def hdf5_to_timed_dataframe( time_stamp_alias: str = "timeStamps", ms_markers_key: str = "msMarkers", first_event_time_stamp_key: str = "FirstEventTimeStamp", - **kwds, + test_fid: int = 0, ) -> ddf.DataFrame: """Function to read a selection of hdf5-files, and generate a delayed dask dataframe from provided groups in the files. Optionally, aliases can be defined. @@ -154,12 +154,12 @@ def hdf5_to_timed_dataframe( Defaults to "msMarkers". first_event_time_stamp_key (str): h5 attribute containing the start timestamp of a file. Defaults to "FirstEventTimeStamp". + test_fid(int, optional): File ID to use for extracting shape information. Returns: ddf.DataFrame: The delayed Dask DataFrame """ # Read a file to parse the file structure - test_fid = kwds.pop("test_fid", 0) test_proc = h5py.File(files[test_fid]) if channels is None: @@ -595,8 +595,7 @@ def read_dataframe( the dataframe from ms-Markers in the files. Defaults to False. **kwds: Keyword parameters. - - **hdf5_groupnames** : List of groupnames to look for in the file. - - **hdf5_aliases**: Dictionary of aliases for the groupnames. + - **channels** : Dict of channel informations. - **time_stamp_alias**: Alias for the timestamp column - **ms_markers_key**: HDF5 path of the millisecond marker column. - **first_event_time_stamp_key**: Attribute name containing the start @@ -613,7 +612,7 @@ def read_dataframe( dataframe and metadata read from specified files. """ # if runs is provided, try to locate the respective files relative to the provided folder. - if runs is not None: # pylint: disable=duplicate-code + if runs is not None: files = [] if isinstance(runs, (str, int)): runs = [runs] @@ -628,7 +627,6 @@ def read_dataframe( metadata=metadata, ) else: - # pylint: disable=duplicate-code super().read_dataframe( files=files, folders=folders, @@ -696,7 +694,7 @@ def get_files_from_run_id( run_id: str, folders: str | Sequence[str] = None, extension: str = "h5", - **kwds, # noqa: ARG002 + **kwds, ) -> list[str]: """Locate the files for a given run identifier. @@ -705,11 +703,16 @@ def get_files_from_run_id( folders (str | Sequence[str], optional): The directory(ies) where the raw data is located. Defaults to config["core"]["base_folder"] extension (str, optional): The file extension. Defaults to "h5". - kwds: Keyword arguments + kwds: Keyword arguments, not used in this loader. Return: list[str]: List of file path strings to the location of run data. """ + if len(kwds) > 0: + raise TypeError( + f"get_files_from_run_id() got unexpected keyword arguments {kwds.keys()}.", + ) + if folders is None: folders = self._config["core"]["paths"]["data_raw_dir"] @@ -979,6 +982,9 @@ def get_count_rate( ), ) + if len(kwds) > 0: + raise TypeError(f"get_count_rate() got unexpected keyword arguments {kwds.keys()}.") + secs_list = [] count_rate_list = [] accumulated_time = 0 @@ -1021,6 +1027,9 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float: ), ) + if len(kwds) > 0: + raise TypeError(f"get_elapsed_time() got unexpected keyword arguments {kwds.keys()}.") + secs = 0.0 for fid in fids: secs += get_elapsed_time( diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 3e20e599..71183a66 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -141,6 +141,11 @@ def get_files_from_run_id( daq = kwds.pop("daq", self._config.get("dataframe", {}).get("daq")) + if len(kwds) > 0: + raise TypeError( + f"get_files_from_run_id() got unexpected keyword arguments {kwds.keys()}.", + ) + stream_name_postfix = stream_name_postfixes.get(daq, "") # Generate the file patterns to search for in the directory file_pattern = f"**/{stream_name_prefixes[daq]}{run_id}{stream_name_postfix}*." + extension @@ -931,6 +936,7 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. + **kwds: Keyword arguments passed to ``parquet_handler``. Returns: tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame, diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index 437a8082..366c7d9d 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -423,6 +423,10 @@ def test_momentum_calibration_two_points() -> None: # Test with passing calibration parameters calibration = mc.calibration.copy() calibration.pop("creation_date") + calibration.pop("grid") + calibration.pop("extent") + calibration.pop("kx_axis") + calibration.pop("ky_axis") df, _, _ = get_loader(loader_name="mpes", config=config).read_dataframe( folders=df_folder, collect_metadata=False, From 77692d8e2f53e0e61a056d9f1631943d70022aec Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 4 Jul 2024 22:17:04 +0200 Subject: [PATCH 139/300] add tests for illegal keyword errors --- tests/test_processor.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_processor.py b/tests/test_processor.py index 42e583b2..43cd4815 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -146,6 +146,19 @@ def test_additional_parameter_to_loader() -> None: ) assert processor.files[0].find("json") > -1 + # check that illegal keywords raise: + with pytest.raises(TypeError): + processor = SedProcessor( + folder=df_folder_generic, + ftype="json", + config=config, + folder_config={}, + user_config={}, + system_config={}, + verbose=True, + illegal_keyword=True, + ) + def test_repr() -> None: """test the ___repr___ method""" @@ -166,6 +179,9 @@ def test_repr() -> None: assert processor_str.find("ADC") > 0 assert processor_str.find("key1") > 0 + with pytest.raises(TypeError): + processor.load(files=files, metadata={"test": {"key1": "value1"}}, illegal_keyword=True) + def test_attributes_setters() -> None: """Test class attributes and setters.""" @@ -228,6 +244,17 @@ def test_copy_tool() -> None: processor.load(files=files) assert processor.files[0].find(dest_folder) > -1 + # test illegal keywords: + config["core"]["copy_tool_kwds"] = {"gid": os.getgid(), "illegal_keyword": True} + with pytest.raises(TypeError): + processor = SedProcessor( + config=config, + folder_config={}, + user_config={}, + system_config={}, + verbose=True, + ) + feature4 = np.array([[203.2, 341.96], [299.16, 345.32], [304.38, 149.88], [199.52, 152.48]]) feature5 = np.array( From f6c33e650a1bf533491f6e87ba2162d01093bafb Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 2 Jul 2024 16:35:57 +0200 Subject: [PATCH 140/300] fix group_name error --- sed/loader/flash/dataframe.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 2af2f2fe..c19bb3b1 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -51,17 +51,13 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: ValueError: If 'index_key' and 'dataset_key' are not provided. """ channel_config = self._config["channels"][channel] - + group_err = "" if "index_key" in channel_config and "dataset_key" in channel_config: return channel_config["index_key"], channel_config["dataset_key"] elif "group_name" in channel_config: - print("'group_name' is no longer supported.") - - raise ValueError( - "For channel:", - channel, - "Provide both 'index_key' and 'dataset_key'.", - ) + group_err = "'group_name' is no longer supported." + error = f"{group_err} For channel: {channel}, provide both 'index_key' and 'dataset_key'." + raise ValueError(error) def get_dataset_array( self, From 30e7447e95d12c15bf59a59b54ecb18458847610 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 2 Jul 2024 23:24:24 +0200 Subject: [PATCH 141/300] bring back old behavior --- sed/loader/flash/buffer_handler.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 7c66f8f2..087b9008 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -129,6 +129,11 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Create a DataFrameCreator instance and the h5 file df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df + # Drop rows with nan values in electron channels + df = df.dropna( + subset=get_channels(self._config["channels"], ["per_electron"]), + ) + # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) @@ -182,11 +187,6 @@ def _fill_dataframes(self): "iterations": self._config.get("forward_fill_iterations", 2), } - # Drop rows with nan values in electron channels - df_electron = dataframe.dropna( - subset=get_channels(self._config["channels"], ["per_electron"]), - ) - # Set the dtypes of the channels here as there should be no null values channel_dtypes = get_channels(self._config["channels"], "all") config_channels = self._config["channels"] @@ -199,13 +199,14 @@ def _fill_dataframes(self): # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): df_electron, meta = split_dld_time_from_sector_id( - df_electron, + dataframe, config=self._config, ) self.metadata.update(meta) self.df_electron = df_electron.astype(dtypes) - self.df_pulse = dataframe[index + fill_channels] + df_pulse = dataframe[index + fill_channels] + self.df_pulse = df_pulse[df_pulse["electronId"] == 0] def run( self, From 49d93e9582565978fde67641ac128767c42c816a Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 3 Jul 2024 01:30:00 +0200 Subject: [PATCH 142/300] minor changes --- sed/loader/flash/buffer_handler.py | 2 +- sed/loader/flash/dataframe.py | 6 ++++++ sed/loader/flash/utils.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 087b9008..eb6a578b 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -131,7 +131,7 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Drop rows with nan values in electron channels df = df.dropna( - subset=get_channels(self._config["channels"], ["per_electron"]), + subset=get_channels(self._config["channels"], "per_electron"), ) # Reset the index of the DataFrame and save it as a parquet file diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index c19bb3b1..b6277100 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -203,6 +203,12 @@ def df_pulse(self) -> pd.DataFrame: series = [] # Get the relevant channel names channels = get_channels(self._config["channels"], "per_pulse") + # check if dldAux is in the channels and raise error if so + if "dldAux" in channels: + raise ValueError( + "dldAux is a 'per_train' channel. " + "Please choose 'per_train' as the format for dldAux.", + ) # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 76af4150..dfed76b1 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -70,7 +70,7 @@ def get_channels( for key in available_channels if channel_dict[key]["format"] == format_ and key != DLD_AUX_ALIAS ) - # Include 'dldAuxChannels' if the format is 'per_pulse' and extend_aux is True. + # Include 'dldAuxChannels' if the format is 'per_train' and extend_aux is True. # Otherwise, include 'dldAux'. if format_ == FORMATS[2] and DLD_AUX_ALIAS in available_channels: if extend_aux: From d830e8c1cb21fd5a8594aa83fa4e50407b0ed287 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 3 Jul 2024 15:54:02 +0200 Subject: [PATCH 143/300] use concat for faster join --- sed/loader/flash/dataframe.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index b6277100..44548ea0 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -281,7 +281,7 @@ def validate_channel_keys(self) -> None: @property def df(self) -> pd.DataFrame: """ - Joins the 'per_electron', 'per_pulse', and 'per_train' using join operation, + Joins the 'per_electron', 'per_pulse', and 'per_train' using concat operation, returning a single dataframe. Returns: @@ -289,8 +289,6 @@ def df(self) -> pd.DataFrame: """ self.validate_channel_keys() - return ( - self.df_electron.join(self.df_pulse, on=self.multi_index, how="outer") - .join(self.df_train, on=self.multi_index, how="outer") - .sort_index() - ) + # been tested with merge, join and concat + # concat offers best performance, almost 3 times faster + return pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() From c48a6ec1bba3556e844c05d581e80cf9b6ed9b41 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 4 Jul 2024 01:38:35 +0200 Subject: [PATCH 144/300] try saving both --- sed/loader/flash/buffer_handler.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index eb6a578b..3431ba06 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -137,6 +137,31 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) + def get_dataframes(self, h5_path) -> tuple[dd.DataFrame, dd.DataFrame]: + """ + Returns the electron and pulse dataframes. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + + Returns: + Tuple[dd.DataFrame, dd.DataFrame]: The electron and pulse dataframes. + """ + df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df + fill_channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + df[fill_channels] = df[fill_channels].ffill() + df_electron = df.dropna( + subset=get_channels(self._config["channels"], "per_electron"), + ).reset_index() + df_pulse = df[fill_channels].loc[:, :, 0].reset_index() + + return df_electron, df_pulse + def _save_buffer_files(self, debug: bool) -> None: """ Creates the buffer files. From 4d11d7a8a54b15f8488c5f994b608e25ddd26572 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 4 Jul 2024 02:15:04 +0200 Subject: [PATCH 145/300] change fill section --- sed/loader/flash/buffer_handler.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 3431ba06..6fb6cb4d 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -129,11 +129,6 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Create a DataFrameCreator instance and the h5 file df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df - # Drop rows with nan values in electron channels - df = df.dropna( - subset=get_channels(self._config["channels"], "per_electron"), - ) - # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) @@ -220,11 +215,13 @@ def _fill_dataframes(self): for channel in channel_dtypes if config_channels[channel].get("dtype") is not None } - + df_electron = dataframe.dropna( + subset=get_channels(self._config["channels"], "per_electron"), + ) # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): df_electron, meta = split_dld_time_from_sector_id( - dataframe, + df_electron, config=self._config, ) self.metadata.update(meta) From 88f0e9ccb30e3ad18cf7fc0be6755cf45c26f1f5 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 4 Jul 2024 10:15:17 +0200 Subject: [PATCH 146/300] parallel processing --- sed/loader/flash/buffer_handler.py | 45 +++++++++++++++++++++++++++++- sed/loader/flash/loader.py | 21 +++++++------- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 6fb6cb4d..6bf92add 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -132,7 +132,7 @@ def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: # Reset the index of the DataFrame and save it as a parquet file df.reset_index().to_parquet(parquet_path) - def get_dataframes(self, h5_path) -> tuple[dd.DataFrame, dd.DataFrame]: + def get_dataframe(self, h5_path) -> tuple[dd.DataFrame, dd.DataFrame]: """ Returns the electron and pulse dataframes. @@ -157,6 +157,49 @@ def get_dataframes(self, h5_path) -> tuple[dd.DataFrame, dd.DataFrame]: return df_electron, df_pulse + def get_dataframes(self, h5_paths): + """ + Returns the electron and pulse dataframes. + + Args: + h5_paths (List[Path]): List of paths to H5 files. + folder (Path): Path to the folder for buffer files. + + Returns: + Tuple[dd.DataFrame, dd.DataFrame]: The electron and pulse dataframes. + """ + n_cores = min(len(h5_paths), self.n_cores) + + # Helper function to process each h5_path + def process_path(h5_path): + df_electron, df_pulse = self.get_dataframe(h5_path) + length = len(df_electron) + df_electron = dd.from_pandas(df_electron, npartitions=1) + df_pulse = dd.from_pandas(df_pulse, npartitions=1) + return df_electron, df_pulse, length + + # Parallelize the loop with joblib + results = Parallel(n_jobs=n_cores, verbose=10)( + delayed(process_path)(h5_path) for h5_path in h5_paths + ) + + # Unpack results + df_electrons, df_pulses, lengths = zip(*results) + + df_electron = dd.concat(list(df_electrons)) + fill_channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + self.df_electron = forward_fill_lazy( + df=df_electron, + columns=fill_channels, + before=min(lengths), + iterations=1, + ) + self.df_pulse = dd.concat(list(df_pulses)) + def _save_buffer_files(self, debug: bool) -> None: """ Creates the buffer files. diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 01d2aa62..6e627a61 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -264,10 +264,10 @@ def read_dataframe( ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, - detector: str = "", - force_recreate: bool = False, + # detector: str = "", + # force_recreate: bool = False, parquet_dir: str | Path = None, - debug: bool = False, + # debug: bool = False, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -329,13 +329,14 @@ def read_dataframe( # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] - bh.run( - h5_paths=h5_paths, - folder=parquet_dir, - force_recreate=force_recreate, - suffix=detector, - debug=debug, - ) + # bh.run( + # h5_paths=h5_paths, + # folder=parquet_dir, + # force_recreate=force_recreate, + # suffix=detector, + # debug=debug, + # ) + bh.get_dataframes(h5_paths) df = bh.df_electron df_timed = bh.df_pulse From f6a45e5906033ffc410c06f415cb51d37c268408 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 4 Jul 2024 12:27:00 +0200 Subject: [PATCH 147/300] change paths config to hold raw and processed keys --- benchmarks/benchmark_sed.py | 13 ++++--- sed/config/flash_example_config.yaml | 4 +- sed/loader/flash/loader.py | 45 +++++++++++------------ tests/data/loader/flash/config.yaml | 4 +- tests/loader/flash/test_buffer_handler.py | 2 +- tests/loader/flash/test_flash_loader.py | 11 ++++-- 6 files changed, 42 insertions(+), 37 deletions(-) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index 3b633c30..9ab5b003 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -54,7 +54,7 @@ def test_binning_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_1d"]: print(f"Updating targets for 'binning_1d' to {float(np.mean(result))}") @@ -78,7 +78,7 @@ def test_binning_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_4d"]: print(f"Updating targets for 'binning_4d' to {float(np.mean(result))}") @@ -103,7 +103,7 @@ def test_splinewarp() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin + assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["inv_dfield"]: print(f"Updating targets for 'inv_dfield' to {float(np.mean(result))}") @@ -137,7 +137,7 @@ def test_workflow_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_1d"]: print(f"Updating targets for 'workflow_1d' to {float(np.mean(result))}") @@ -171,7 +171,7 @@ def test_workflow_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_4d"]: print(f"Updating targets for 'workflow_4d' to {float(np.mean(result))}") @@ -188,6 +188,7 @@ def test_loader_compute(loader: BaseLoader) -> None: loaded_dataframe, _, loaded_metadata = loader.read_dataframe( runs=runs[loader_name], collect_metadata=False, + force_recreate=True, ) loaded_dataframe.compute() @@ -197,7 +198,7 @@ def test_loader_compute(loader: BaseLoader) -> None: ) result = timer.repeat(20, number=1) print(result) - assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin + assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets[f"loader_compute_{loader_name}"]: print( diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 7f4cf51b..a671f0d5 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -16,9 +16,9 @@ core: # provided, the loader will try to find the data based on year beamtimeID etc paths: # location of the raw data. - data_raw_dir: "tests/data/loader/flash/" + raw: "tests/data/loader/flash/" # location of the intermediate parquet files. - data_parquet_dir: "tests/data/loader/flash/parquet" + processed: "tests/data/loader/flash/parquet" binning: # Since this will run on maxwell most probably, we have a lot of cores at our disposal diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 6e627a61..3f21f0c4 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -44,7 +44,7 @@ def __init__(self, config: dict) -> None: super().__init__(config=config) self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof self.raw_dir: str = None - self.parquet_dir: str = None + self.processed_dir: str = None def _initialize_dirs(self) -> None: """ @@ -59,12 +59,11 @@ def _initialize_dirs(self) -> None: FileNotFoundError: If the raw data directories are not found. """ # Parses to locate the raw beamtime directory from config file + # Only raw_dir is necessary, processed_dir can be based on raw_dir, if not provided if "paths" in self._config["core"]: - data_raw_dir = [ - Path(self._config["core"]["paths"].get("data_raw_dir", "")), - ] - data_parquet_dir = Path( - self._config["core"]["paths"].get("data_parquet_dir", ""), + raw_dir = Path(self._config["core"]["paths"].get("raw", "")) + processed_dir = Path( + self._config["core"]["paths"].get("processed", raw_dir.joinpath("processed")), ) else: @@ -83,27 +82,27 @@ def _initialize_dirs(self) -> None: beamtime_dir = beamtime_dir.joinpath(f"{year}/data/{beamtime_id}/") # Use pathlib walk to reach the raw data directory - data_raw_dir = [] - raw_path = beamtime_dir.joinpath("raw") + raw_paths: list[Path] = [] - for path in raw_path.glob("**/*"): + for path in beamtime_dir.joinpath("raw").glob("**/*"): if path.is_dir(): dir_name = path.name if dir_name.startswith(("online-", "express-")): - data_raw_dir.append(path.joinpath(self._config["dataframe"]["daq"])) + raw_paths.append(path.joinpath(self._config["dataframe"]["daq"])) elif dir_name == self._config["dataframe"]["daq"].upper(): - data_raw_dir.append(path) + raw_paths.append(path) - if not data_raw_dir: + if not raw_paths: raise FileNotFoundError("Raw data directories not found.") - parquet_path = "processed/parquet" - data_parquet_dir = beamtime_dir.joinpath(parquet_path) + raw_dir = raw_paths[0].resolve() - data_parquet_dir.mkdir(parents=True, exist_ok=True) + processed_dir = beamtime_dir.joinpath("processed") - self.raw_dir = str(data_raw_dir[0].resolve()) - self.parquet_dir = str(data_parquet_dir) + processed_dir.mkdir(parents=True, exist_ok=True) + + self.raw_dir = str(raw_dir) + self.processed_dir = str(processed_dir) @property def available_runs(self) -> list[int]: @@ -266,7 +265,7 @@ def read_dataframe( collect_metadata: bool = False, # detector: str = "", # force_recreate: bool = False, - parquet_dir: str | Path = None, + processed_dir: str | Path = None, # debug: bool = False, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: @@ -290,7 +289,7 @@ def read_dataframe( and metadata. Raises: - ValueError: If neither 'runs' nor 'files'/'data_raw_dir' is provided. + ValueError: If neither 'runs' nor 'files'/'raw_dir' is provided. FileNotFoundError: If the conversion fails for some files or no data is available. """ t0 = time.time() @@ -322,16 +321,16 @@ def read_dataframe( config=self._config, ) - # if parquet_dir is None, use self.parquet_dir - parquet_dir = parquet_dir or self.parquet_dir - parquet_dir = Path(parquet_dir) + # if processed_dir is None, use self.processed_dir + processed_dir = processed_dir or self.processed_dir + processed_dir = Path(processed_dir) # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] # bh.run( # h5_paths=h5_paths, - # folder=parquet_dir, + # folder=processed_dir, # force_recreate=force_recreate, # suffix=detector, # debug=debug, diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 1fcad9b7..0a9a8170 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -9,8 +9,8 @@ core: # The paths to the raw and parquet data directories. paths: - data_raw_dir: "tests/data/loader/flash/" - data_parquet_dir: "tests/data/loader/flash/parquet" + raw: "tests/data/loader/flash/" + processed: "tests/data/loader/flash/parquet" # These can be replaced by beamtime_id and year to automatically # find the folders on the desy cluster diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 719144eb..eb4d2c6f 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -17,7 +17,7 @@ def create_parquet_dir(config: dict, folder: str) -> Path: and folder name. """ - parquet_path = Path(config["core"]["paths"]["data_parquet_dir"]) + parquet_path = Path(config["core"]["paths"]["processed"]) parquet_path = parquet_path.joinpath(folder) parquet_path.mkdir(parents=True, exist_ok=True) return parquet_path diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 4fec8c8c..14757341 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -39,7 +39,7 @@ def test_initialize_dirs( ) # Create expected paths expected_raw_path = expected_path / "raw" / "hdf" / sub_dir - expected_processed_path = expected_path / "processed" / "parquet" + expected_processed_path = expected_path / "processed" # Create a fake file system for testing fs.create_dir(expected_raw_path) @@ -49,7 +49,7 @@ def test_initialize_dirs( fl = FlashLoader(config=config_) fl._initialize_dirs() assert str(expected_raw_path) == fl.raw_dir - assert str(expected_processed_path) == fl.parquet_dir + assert str(expected_processed_path) == fl.processed_dir # remove beamtime_id, year and daq from config to raise error del config_["core"]["beamtime_id"] @@ -89,7 +89,7 @@ def test_save_read_parquet_flash(config: dict) -> None: """ config_ = config.copy() data_parquet_dir = create_parquet_dir(config_, "flash_save_read") - config_["core"]["paths"]["data_parquet_dir"] = data_parquet_dir + config_["core"]["paths"]["processed"] = data_parquet_dir fl = FlashLoader(config=config_) # First call: should create and read the parquet file @@ -174,6 +174,11 @@ def test_get_elapsed_time_fid(config: dict) -> None: def test_get_elapsed_time_run(config: dict) -> None: + config_ = config.copy() + config_["core"]["paths"] = { + "raw": "tests/data/loader/flash/", + "processed": "tests/data/loader/flash/parquet/get_elapsed_time_run", + } """Test get_elapsed_time method of FlashLoader class""" config_ = config.copy() data_parquet_dir = create_parquet_dir(config_, "get_elapsed_time_run") From a8632abda9f60526844d5aee2d04e9cf619d252b Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 4 Jul 2024 20:55:33 +0200 Subject: [PATCH 148/300] change filehandling of buffer files as it's more complicated now, and filling for both electron and pulse --- sed/loader/flash/buffer_handler.py | 271 +++++++++++++---------------- sed/loader/flash/loader.py | 21 ++- sed/loader/flash/utils.py | 53 ------ tests/loader/flash/test_utils.py | 38 ---- 4 files changed, 134 insertions(+), 249 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 6bf92add..384474f5 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from itertools import compress from pathlib import Path import dask.dataframe as dd @@ -12,11 +11,74 @@ from sed.core.dfops import forward_fill_lazy from sed.loader.flash.dataframe import DataFrameCreator from sed.loader.flash.utils import get_channels -from sed.loader.flash.utils import initialize_paths from sed.loader.utils import get_parquet_metadata from sed.loader.utils import split_dld_time_from_sector_id +class FilePaths: + """ + A class for handling the paths to the raw and buffer files of electron and timed dataframes. + A list of file sets (dict) are created for each H5 file containing the paths to the raw file + and the electron and timed buffer files. + """ + + SUBDIRECTORIES = ["electron", "timed"] + + def __init__(self, h5_paths: list[Path], processed_folder: Path, suffix: str) -> None: + suffix = f"_{suffix}" if suffix else "" + buffer_folder = processed_folder / "buffer" + + # Create subdirectories if they do not exist + for subfolder in self.SUBDIRECTORIES: + (buffer_folder / subfolder).mkdir(parents=True, exist_ok=True) + + # a list of file sets containing the paths to the raw, electron and timed buffer files + self._file_paths = [ + { + "raw": h5_path, + **{ + subfolder: buffer_folder / subfolder / f"{h5_path.stem}{suffix}" + for subfolder in self.SUBDIRECTORIES + }, + } + for h5_path in h5_paths + ] + + def __getitem__(self, key) -> list[Path] | dict[str, Path]: + if isinstance(key, str): + return [file_set[key] for file_set in self._file_paths] + return self._file_paths[key] + + def __iter__(self): + return iter(self._file_paths) + + def __len__(self): + return len(self._file_paths) + + @property + def raw(self): + return self["raw"] + + @property + def electron(self): + return self["electron"] + + @property + def timed(self): + return self["timed"] + + def to_process(self, force_recreate: bool = False) -> list[dict[str, Path]]: + """Returns a list of file sets that need to be processed.""" + if not force_recreate: + return [ + file_set + for file_set in self + if any(not file_set[key].exists() for key in self.SUBDIRECTORIES) + ] + else: + return list(self) + + class BufferHandler: """ A class for handling the creation and manipulation of buffer files using DataFrameCreator. @@ -32,13 +94,9 @@ def __init__( Args: config (dict): The configuration dictionary. """ - self._config = config["dataframe"] - self.n_cores = config["core"].get("num_cores", os.cpu_count() - 1) - - self.buffer_paths: list[Path] = [] - self.missing_h5_files: list[Path] = [] - self.save_paths: list[Path] = [] - + self._config: dict = config["dataframe"] + self.n_cores: int = config["core"].get("num_cores", os.cpu_count() - 1) + self.file_paths: FilePaths = None self.df_electron: dd.DataFrame = None self.df_pulse: dd.DataFrame = None self.metadata: dict = {} @@ -50,13 +108,14 @@ def _schema_check(self) -> None: Raises: ValueError: If the schema of the Parquet files does not match the configuration. """ - existing_parquet_filenames = [file for file in self.buffer_paths if file.exists()] - parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] + buffer_filenames = self.file_paths.electron + self.file_paths.timed + existing = [file for file in buffer_filenames if file.exists()] + parquet_schemas = [pq.read_schema(file) for file in existing] config_schema_set = set( get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), ) - for filename, schema in zip(existing_parquet_filenames, parquet_schemas): + for filename, schema in zip(existing, parquet_schemas): # for retro compatibility when sectorID was also saved in buffer if self._config["sector_id_column"] in schema.names: config_schema_set.add( @@ -79,153 +138,78 @@ def _schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def _get_files_to_read( - self, - h5_paths: list[Path], - folder: Path, - prefix: str, - suffix: str, - force_recreate: bool, - ) -> None: + def _save_buffer_file(self, paths: dict[str, Path]) -> None: """ - Determines the list of files to read and the corresponding buffer files to create. + Creates the electron and timed buffer files from the raw H5 file. + First the dataframe is accessed and forward filled in the non-electron channels. + Then the data types are set. For the electron dataframe, all values not in the electron + channels are dropped. For the timed dataframe, only the train and pulse channels are taken + and it pulse resolved (no longer electron resolved). Both are saved as parquet files. Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - prefix (str): Prefix for buffer file names. - suffix (str): Suffix for buffer file names. - force_recreate (bool): Flag to force recreation of buffer files. - """ - # Getting the paths of the buffer files, with subfolder as buffer and no extension - self.buffer_paths = initialize_paths( - filenames=[h5_path.stem for h5_path in h5_paths], - folder=folder, - subfolder="buffer", - prefix=prefix, - suffix=suffix, - extension="", - ) - # read only the files that do not exist or if force_recreate is True - files_to_read = [ - force_recreate or not parquet_path.exists() for parquet_path in self.buffer_paths - ] - - # Get the list of H5 files to read and the corresponding buffer files to create - self.missing_h5_files = list(compress(h5_paths, files_to_read)) - self.save_paths = list(compress(self.buffer_paths, files_to_read)) - - print(f"Reading files: {len(self.missing_h5_files)} new files of {len(h5_paths)} total.") - - def _save_buffer_file(self, h5_path: Path, parquet_path: Path) -> None: - """ - Creates a single buffer file. - - Args: - h5_path (Path): Path to the H5 file. - parquet_path (Path): Path to the buffer file. + paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. """ # Create a DataFrameCreator instance and the h5 file - df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df - - # Reset the index of the DataFrame and save it as a parquet file - df.reset_index().to_parquet(parquet_path) - - def get_dataframe(self, h5_path) -> tuple[dd.DataFrame, dd.DataFrame]: - """ - Returns the electron and pulse dataframes. + df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df - Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. + config_channels = self._config["channels"] - Returns: - Tuple[dd.DataFrame, dd.DataFrame]: The electron and pulse dataframes. - """ - df = DataFrameCreator(config_dataframe=self._config, h5_path=h5_path).df fill_channels: list[str] = get_channels( - self._config["channels"], + config_channels, ["per_pulse", "per_train"], extend_aux=True, ) + # forward fill all the non-electron channels df[fill_channels] = df[fill_channels].ffill() - df_electron = df.dropna( - subset=get_channels(self._config["channels"], "per_electron"), - ).reset_index() - df_pulse = df[fill_channels].loc[:, :, 0].reset_index() - - return df_electron, df_pulse - - def get_dataframes(self, h5_paths): - """ - Returns the electron and pulse dataframes. - - Args: - h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. - Returns: - Tuple[dd.DataFrame, dd.DataFrame]: The electron and pulse dataframes. - """ - n_cores = min(len(h5_paths), self.n_cores) - - # Helper function to process each h5_path - def process_path(h5_path): - df_electron, df_pulse = self.get_dataframe(h5_path) - length = len(df_electron) - df_electron = dd.from_pandas(df_electron, npartitions=1) - df_pulse = dd.from_pandas(df_pulse, npartitions=1) - return df_electron, df_pulse, length - - # Parallelize the loop with joblib - results = Parallel(n_jobs=n_cores, verbose=10)( - delayed(process_path)(h5_path) for h5_path in h5_paths - ) + # Reset the index of the DataFrame and save it as a parquet file + # Set the dtypes + channel_dtypes = get_channels(config_channels, "all") + dtypes = { + channel: config_channels[channel].get("dtype") + for channel in channel_dtypes + if config_channels[channel].get("dtype") is not None + } - # Unpack results - df_electrons, df_pulses, lengths = zip(*results) + # electron resolved dataframe + df.dropna( + subset=get_channels(self._config["channels"], "per_electron"), + ).reset_index().astype(dtypes).to_parquet(paths["electron"]) - df_electron = dd.concat(list(df_electrons)) - fill_channels: list[str] = get_channels( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) - self.df_electron = forward_fill_lazy( - df=df_electron, - columns=fill_channels, - before=min(lengths), - iterations=1, - ) - self.df_pulse = dd.concat(list(df_pulses)) + # timed resolved dataframe + df[fill_channels].loc[:, :, 0].reset_index().astype(dtypes).to_parquet(paths["timed"]) - def _save_buffer_files(self, debug: bool) -> None: + def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ - Creates the buffer files. + Creates the buffer files that are missing. Args: + force_recreate (bool): Flag to force recreation of buffer files. debug (bool): Flag to enable debug mode, which serializes the creation. """ - n_cores = min(len(self.missing_h5_files), self.n_cores) - paths = zip(self.missing_h5_files, self.save_paths) + to_process = self.file_paths.to_process(force_recreate) + print(f"Reading files: {len(to_process)} new files of {len(self.file_paths)} total.") + n_cores = min(len(to_process), self.n_cores) if n_cores > 0: if debug: - for h5_path, parquet_path in paths: - self._save_buffer_file(h5_path, parquet_path) + for file_set in to_process: + self._save_buffer_file(file_set) else: Parallel(n_jobs=n_cores, verbose=10)( - delayed(self._save_buffer_file)(h5_path, parquet_path) - for h5_path, parquet_path in paths + delayed(self._save_buffer_file)(file_set) for file_set in to_process ) def _fill_dataframes(self): """ Reads all parquet files into one dataframe using dask and fills NaN values. """ - dataframe = dd.read_parquet(self.buffer_paths, calculate_divisions=True) + + df_electron = dd.read_parquet(self.file_paths.electron, calculate_divisions=True) + df_pulse = dd.read_parquet(self.file_paths.timed, calculate_divisions=True) + file_metadata = get_parquet_metadata( - self.buffer_paths, + self.file_paths.electron, time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), ) self.metadata["file_statistics"] = file_metadata @@ -235,32 +219,28 @@ def _fill_dataframes(self): ["per_pulse", "per_train"], extend_aux=True, ) - index: list[str] = get_channels(index=True) overlap = min(file["num_rows"] for file in file_metadata.values()) - dataframe = forward_fill_lazy( - df=dataframe, + df_electron = forward_fill_lazy( + df=df_electron, columns=fill_channels, before=overlap, iterations=self._config.get("forward_fill_iterations", 2), ) + self.metadata["forward_fill"] = { "columns": fill_channels, "overlap": overlap, "iterations": self._config.get("forward_fill_iterations", 2), } - # Set the dtypes of the channels here as there should be no null values - channel_dtypes = get_channels(self._config["channels"], "all") - config_channels = self._config["channels"] - dtypes = { - channel: config_channels[channel].get("dtype") - for channel in channel_dtypes - if config_channels[channel].get("dtype") is not None - } - df_electron = dataframe.dropna( - subset=get_channels(self._config["channels"], "per_electron"), + df_pulse = forward_fill_lazy( + df=df_pulse, + columns=fill_channels, + before=overlap, + iterations=self._config.get("forward_fill_iterations", 2), ) + # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): df_electron, meta = split_dld_time_from_sector_id( @@ -269,16 +249,14 @@ def _fill_dataframes(self): ) self.metadata.update(meta) - self.df_electron = df_electron.astype(dtypes) - df_pulse = dataframe[index + fill_channels] - self.df_pulse = df_pulse[df_pulse["electronId"] == 0] + self.df_electron = df_electron + self.df_pulse = df_pulse def run( self, h5_paths: list[Path], folder: Path, force_recreate: bool = False, - prefix: str = "", suffix: str = "", debug: bool = False, ) -> None: @@ -287,18 +265,17 @@ def run( Args: h5_paths (List[Path]): List of paths to H5 files. - folder (Path): Path to the folder for buffer files. + folder (Path): Path to the folder for processed files. force_recreate (bool): Flag to force recreation of buffer files. - prefix (str): Prefix for buffer file names. suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode.): """ - self._get_files_to_read(h5_paths, folder, prefix, suffix, force_recreate) + self.file_paths = FilePaths(h5_paths, folder, suffix) if not force_recreate: self._schema_check() - self._save_buffer_files(debug) + self._save_buffer_files(force_recreate, debug) self._fill_dataframes() diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 3f21f0c4..3bb4b39b 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -263,10 +263,10 @@ def read_dataframe( ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, - # detector: str = "", - # force_recreate: bool = False, + detector: str = "", + force_recreate: bool = False, processed_dir: str | Path = None, - # debug: bool = False, + debug: bool = False, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -328,14 +328,13 @@ def read_dataframe( # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] - # bh.run( - # h5_paths=h5_paths, - # folder=processed_dir, - # force_recreate=force_recreate, - # suffix=detector, - # debug=debug, - # ) - bh.get_dataframes(h5_paths) + bh.run( + h5_paths=h5_paths, + folder=processed_dir, + force_recreate=force_recreate, + suffix=detector, + debug=debug, + ) df = bh.df_electron df_timed = bh.df_pulse diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index dfed76b1..6b65c1e4 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path # TODO: move to config MULTI_INDEX = ["trainId", "pulseId", "electronId"] @@ -81,55 +80,3 @@ def get_channels( channels.extend([DLD_AUX_ALIAS]) return channels - - -def initialize_paths( - filenames: str | list[str] = None, - folder: Path = None, - subfolder: str = "", - prefix: str = "", - suffix: str = "", - extension: str = "parquet", - paths: list[Path] = None, -) -> list[Path]: - """ - Initialize the paths for files to be saved/loaded. - - If custom paths are provided, they will be used. Otherwise, paths will be generated based on - the specified parameters during initialization. - - Args: - filenames (str | list[str]): The name(s) of the file(s). - folder (Path): The folder where the files are saved. - subfolder (str): The subfolder where the files are saved. - prefix (str): The prefix for the file name. - suffix (str): The suffix for the file name. - extension (str): The extension for the file. - paths (list[Path]): Custom paths for the files. - - Returns: - list[Path]: The paths for the files. - """ - # if filenames is string, convert it to a list - if isinstance(filenames, str): - filenames = [filenames] - - # Check if the folder and Parquet paths are provided - if not folder and not paths: - raise ValueError("Please provide folder or paths.") - if folder and not filenames: - raise ValueError("With folder, please provide filenames.") - - # Otherwise create the full path for the Parquet file - directory = folder.joinpath(subfolder) - directory.mkdir(parents=True, exist_ok=True) - - if extension: - extension = f".{extension}" # if extension is provided, it is prepended with a dot - if prefix: - prefix = f"{prefix}_" - if suffix: - suffix = f"_{suffix}" - paths = [directory.joinpath(Path(f"{prefix}{name}{suffix}{extension}")) for name in filenames] - - return paths diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index 4fc3fee6..82ec048d 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -1,11 +1,5 @@ """Tests for utils functionality""" -from pathlib import Path - -import pytest - -from .test_buffer_handler import create_parquet_dir from sed.loader.flash.utils import get_channels -from sed.loader.flash.utils import initialize_paths # Define expected channels for each format. ELECTRON_CHANNELS = ["dldPosX", "dldPosY", "dldTimeSteps"] @@ -78,35 +72,3 @@ def test_get_channels_by_format(config_dataframe: dict) -> None: ) == set( format_all_index_extend_aux, ) - - -def test_parquet_init_error() -> None: - """Test ParquetHandler initialization error""" - with pytest.raises(ValueError) as e: - _ = initialize_paths(filenames="test") - - assert "Please provide folder or paths." in str(e.value) - - with pytest.raises(ValueError) as e: - _ = initialize_paths(folder=Path("test")) - - assert "With folder, please provide filenames." in str(e.value) - - -def test_initialize_paths(config: dict) -> None: - """Test ParquetHandler initialization""" - folder = create_parquet_dir(config, "parquet_init") - - ph = initialize_paths("test", folder, extension="xyz") - assert ph[0].suffix == ".xyz" - assert ph[0].name == "test.xyz" - - # test prefix and suffix - ph = initialize_paths("test", folder, prefix="prefix", suffix="suffix") - assert ph[0].name == "prefix_test_suffix.parquet" - - # test with list of parquet_names and subfolder - ph = initialize_paths(["test1", "test2"], folder, subfolder="subfolder") - assert ph[0].parent.name == "subfolder" - assert ph[0].name == "test1.parquet" - assert ph[1].name == "test2.parquet" From 53ea8fd5a2fb6f68b681131d61d29a3b2188a068 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 6 Jul 2024 19:27:07 +0200 Subject: [PATCH 149/300] add dtype handler, make more changes to filepath handler --- sed/loader/flash/buffer_handler.py | 58 +++++++++++------------------- sed/loader/flash/utils.py | 21 +++++++++++ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 384474f5..fffdc1ae 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -11,11 +11,12 @@ from sed.core.dfops import forward_fill_lazy from sed.loader.flash.dataframe import DataFrameCreator from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import get_dtypes from sed.loader.utils import get_parquet_metadata from sed.loader.utils import split_dld_time_from_sector_id -class FilePaths: +class BufferFilePaths: """ A class for handling the paths to the raw and buffer files of electron and timed dataframes. A list of file sets (dict) are created for each H5 file containing the paths to the raw file @@ -24,27 +25,27 @@ class FilePaths: SUBDIRECTORIES = ["electron", "timed"] - def __init__(self, h5_paths: list[Path], processed_folder: Path, suffix: str) -> None: + def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: suffix = f"_{suffix}" if suffix else "" - buffer_folder = processed_folder / "buffer" + folder = folder / "buffer" # Create subdirectories if they do not exist for subfolder in self.SUBDIRECTORIES: - (buffer_folder / subfolder).mkdir(parents=True, exist_ok=True) + (folder / subfolder).mkdir(parents=True, exist_ok=True) # a list of file sets containing the paths to the raw, electron and timed buffer files self._file_paths = [ { "raw": h5_path, **{ - subfolder: buffer_folder / subfolder / f"{h5_path.stem}{suffix}" + subfolder: folder / subfolder / f"{h5_path.stem}{suffix}" for subfolder in self.SUBDIRECTORIES }, } for h5_path in h5_paths ] - def __getitem__(self, key) -> list[Path] | dict[str, Path]: + def __getitem__(self, key) -> list[Path]: if isinstance(key, str): return [file_set[key] for file_set in self._file_paths] return self._file_paths[key] @@ -55,18 +56,6 @@ def __iter__(self): def __len__(self): return len(self._file_paths) - @property - def raw(self): - return self["raw"] - - @property - def electron(self): - return self["electron"] - - @property - def timed(self): - return self["timed"] - def to_process(self, force_recreate: bool = False) -> list[dict[str, Path]]: """Returns a list of file sets that need to be processed.""" if not force_recreate: @@ -96,7 +85,7 @@ def __init__( """ self._config: dict = config["dataframe"] self.n_cores: int = config["core"].get("num_cores", os.cpu_count() - 1) - self.file_paths: FilePaths = None + self.fp: BufferFilePaths = None self.df_electron: dd.DataFrame = None self.df_pulse: dd.DataFrame = None self.metadata: dict = {} @@ -108,7 +97,7 @@ def _schema_check(self) -> None: Raises: ValueError: If the schema of the Parquet files does not match the configuration. """ - buffer_filenames = self.file_paths.electron + self.file_paths.timed + buffer_filenames = self.fp["electron"] + self.fp["timed"] existing = [file for file in buffer_filenames if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing] config_schema_set = set( @@ -164,20 +153,15 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: df[fill_channels] = df[fill_channels].ffill() # Reset the index of the DataFrame and save it as a parquet file - # Set the dtypes - channel_dtypes = get_channels(config_channels, "all") - dtypes = { - channel: config_channels[channel].get("dtype") - for channel in channel_dtypes - if config_channels[channel].get("dtype") is not None - } - + electron_channels = get_channels(self._config["channels"], "per_electron") + dtypes = get_dtypes(config_channels, "all", extend_aux=True) # electron resolved dataframe - df.dropna( - subset=get_channels(self._config["channels"], "per_electron"), - ).reset_index().astype(dtypes).to_parquet(paths["electron"]) + df.dropna(subset=electron_channels).reset_index().astype(dtypes).to_parquet( + paths["electron"], + ) # timed resolved dataframe + dtypes = get_dtypes(config_channels, ["per_pulse", "per_train"], extend_aux=True) df[fill_channels].loc[:, :, 0].reset_index().astype(dtypes).to_parquet(paths["timed"]) def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: @@ -188,8 +172,8 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: force_recreate (bool): Flag to force recreation of buffer files. debug (bool): Flag to enable debug mode, which serializes the creation. """ - to_process = self.file_paths.to_process(force_recreate) - print(f"Reading files: {len(to_process)} new files of {len(self.file_paths)} total.") + to_process = self.fp.to_process(force_recreate) + print(f"Reading files: {len(to_process)} new files of {len(self.fp)} total.") n_cores = min(len(to_process), self.n_cores) if n_cores > 0: if debug: @@ -205,11 +189,11 @@ def _fill_dataframes(self): Reads all parquet files into one dataframe using dask and fills NaN values. """ - df_electron = dd.read_parquet(self.file_paths.electron, calculate_divisions=True) - df_pulse = dd.read_parquet(self.file_paths.timed, calculate_divisions=True) + df_electron = dd.read_parquet(self.fp["electron"], calculate_divisions=True) + df_pulse = dd.read_parquet(self.fp["timed"], calculate_divisions=True) file_metadata = get_parquet_metadata( - self.file_paths.electron, + self.fp["electron"], time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), ) self.metadata["file_statistics"] = file_metadata @@ -271,7 +255,7 @@ def run( debug (bool): Flag to enable debug mode.): """ - self.file_paths = FilePaths(h5_paths, folder, suffix) + self.fp = BufferFilePaths(h5_paths, folder, suffix) if not force_recreate: self._schema_check() diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 6b65c1e4..16935d16 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -17,6 +17,7 @@ def get_channels( ) -> list[str]: """ Returns a list of channels associated with the specified format(s). + 'all' returns all channels but 'pulseId' and 'dldAux' (if not extended). Args: formats (str | list[str]): The desired format(s) @@ -80,3 +81,23 @@ def get_channels( channels.extend([DLD_AUX_ALIAS]) return channels + + +def get_dtypes(channels_dict: dict, formats: str | list[str], extend_aux: bool = False) -> dict: + """Returns a dictionary of channels and their corresponding data types. + + Args: + channels_dict (dict): The dictionary containing the channels. + formats (str | list[str]): The desired format(s). + extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, + else includes 'dldAux'. + + Returns: + dict: A dictionary of channels and their corresponding data types. + """ + channels = get_channels(channels_dict, formats, extend_aux) + return { + channel: channels_dict[channel].get("dtype") + for channel in channels + if channels_dict[channel].get("dtype") is not None + } From e43639799011a7f25b2303983390b142b1ecefb8 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jul 2024 19:58:03 +0200 Subject: [PATCH 150/300] update methods and tests --- sed/loader/flash/buffer_handler.py | 180 +++++++++++----------- sed/loader/flash/loader.py | 6 +- sed/loader/flash/utils.py | 15 +- sed/loader/sxp/loader.py | 14 +- tests/data/loader/sxp/config.yaml | 4 +- tests/loader/flash/test_buffer_handler.py | 115 ++++++++------ tests/loader/flash/test_flash_loader.py | 26 ++-- tests/loader/test_loaders.py | 44 +++--- 8 files changed, 214 insertions(+), 190 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index fffdc1ae..0d92e201 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -16,31 +16,40 @@ from sed.loader.utils import split_dld_time_from_sector_id +DF_TYP = ["electron", "timed"] + + class BufferFilePaths: """ A class for handling the paths to the raw and buffer files of electron and timed dataframes. A list of file sets (dict) are created for each H5 file containing the paths to the raw file and the electron and timed buffer files. - """ - SUBDIRECTORIES = ["electron", "timed"] + Structure of the file sets: + { + "raw": Path to the H5 file, + "electron": Path to the electron buffer file, + "timed": Path to the timed buffer file, + } + """ def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: + """Initializes the BufferFilePaths. + + Args: + h5_paths (list[Path]): List of paths to the H5 files. + folder (Path): Path to the folder for processed files. + suffix (str): Suffix for buffer file names. + """ suffix = f"_{suffix}" if suffix else "" folder = folder / "buffer" - - # Create subdirectories if they do not exist - for subfolder in self.SUBDIRECTORIES: - (folder / subfolder).mkdir(parents=True, exist_ok=True) + folder.mkdir(parents=True, exist_ok=True) # a list of file sets containing the paths to the raw, electron and timed buffer files self._file_paths = [ { "raw": h5_path, - **{ - subfolder: folder / subfolder / f"{h5_path.stem}{suffix}" - for subfolder in self.SUBDIRECTORIES - }, + **{typ: folder / f"{typ}_{h5_path.stem}{suffix}" for typ in DF_TYP}, } for h5_path in h5_paths ] @@ -60,9 +69,7 @@ def to_process(self, force_recreate: bool = False) -> list[dict[str, Path]]: """Returns a list of file sets that need to be processed.""" if not force_recreate: return [ - file_set - for file_set in self - if any(not file_set[key].exists() for key in self.SUBDIRECTORIES) + file_set for file_set in self if any(not file_set[key].exists() for key in DF_TYP) ] else: return list(self) @@ -86,34 +93,29 @@ def __init__( self._config: dict = config["dataframe"] self.n_cores: int = config["core"].get("num_cores", os.cpu_count() - 1) self.fp: BufferFilePaths = None - self.df_electron: dd.DataFrame = None - self.df_pulse: dd.DataFrame = None + self.df: dict[str, dd.DataFrame] = {typ: None for typ in DF_TYP} + self.fill_channels: list[str] = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) self.metadata: dict = {} - def _schema_check(self) -> None: + def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: """ Checks the schema of the Parquet files. Raises: ValueError: If the schema of the Parquet files does not match the configuration. """ - buffer_filenames = self.fp["electron"] + self.fp["timed"] - existing = [file for file in buffer_filenames if file.exists()] + existing = [file for file in files if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing] - config_schema_set = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) for filename, schema in zip(existing, parquet_schemas): - # for retro compatibility when sectorID was also saved in buffer - if self._config["sector_id_column"] in schema.names: - config_schema_set.add( - self._config["sector_id_column"], - ) schema_set = set(schema.names) - if schema_set != config_schema_set: - missing_in_parquet = config_schema_set - schema_set - missing_in_config = schema_set - config_schema_set + if schema_set != expected_schema_set: + missing_in_parquet = expected_schema_set - schema_set + missing_in_config = schema_set - expected_schema_set errors = [] if missing_in_parquet: @@ -142,27 +144,21 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: # Create a DataFrameCreator instance and the h5 file df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df - config_channels = self._config["channels"] - - fill_channels: list[str] = get_channels( - config_channels, - ["per_pulse", "per_train"], - extend_aux=True, - ) # forward fill all the non-electron channels - df[fill_channels] = df[fill_channels].ffill() + df[self.fill_channels] = df[self.fill_channels].ffill() - # Reset the index of the DataFrame and save it as a parquet file - electron_channels = get_channels(self._config["channels"], "per_electron") - dtypes = get_dtypes(config_channels, "all", extend_aux=True) + # Reset the index of the DataFrame and save both the electron and timed dataframes # electron resolved dataframe - df.dropna(subset=electron_channels).reset_index().astype(dtypes).to_parquet( + electron_channels = get_channels(self._config["channels"], "per_electron") + dtypes = get_dtypes(self._config["channels"], "all") + df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( paths["electron"], ) - # timed resolved dataframe - dtypes = get_dtypes(config_channels, ["per_pulse", "per_train"], extend_aux=True) - df[fill_channels].loc[:, :, 0].reset_index().astype(dtypes).to_parquet(paths["timed"]) + # timed dataframe + dtypes = get_dtypes(self._config["channels"], ["per_pulse", "per_train"]) + # drop the electron channels and only take rows with the first electronId + df[self.fill_channels].loc[:, :, 0].astype(dtypes).reset_index().to_parquet(paths["timed"]) def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ @@ -188,53 +184,36 @@ def _fill_dataframes(self): """ Reads all parquet files into one dataframe using dask and fills NaN values. """ + # Loop over the electron and timed dataframes + file_stats = {} + filling = {} + for typ in DF_TYP: + # Read the parquet files into a dask dataframe + df = dd.read_parquet(self.fp[typ], calculate_divisions=True) + # Get the metadata from the parquet files + file_stats[typ] = get_parquet_metadata( + self.fp[typ], + time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), + ) - df_electron = dd.read_parquet(self.fp["electron"], calculate_divisions=True) - df_pulse = dd.read_parquet(self.fp["timed"], calculate_divisions=True) - - file_metadata = get_parquet_metadata( - self.fp["electron"], - time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), - ) - self.metadata["file_statistics"] = file_metadata - - fill_channels: list[str] = get_channels( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) - overlap = min(file["num_rows"] for file in file_metadata.values()) - - df_electron = forward_fill_lazy( - df=df_electron, - columns=fill_channels, - before=overlap, - iterations=self._config.get("forward_fill_iterations", 2), - ) - - self.metadata["forward_fill"] = { - "columns": fill_channels, - "overlap": overlap, - "iterations": self._config.get("forward_fill_iterations", 2), - } - - df_pulse = forward_fill_lazy( - df=df_pulse, - columns=fill_channels, - before=overlap, - iterations=self._config.get("forward_fill_iterations", 2), - ) - - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - df_electron, meta = split_dld_time_from_sector_id( - df_electron, - config=self._config, + # Forward fill the non-electron channels across files + overlap = min(file["num_rows"] for file in file_stats[typ].values()) + iterations = self._config.get("forward_fill_iterations", 2) + df = forward_fill_lazy( + df=df, + columns=self.fill_channels, + before=overlap, + iterations=iterations, ) - self.metadata.update(meta) + # TODO: This dict should be returned by forward_fill_lazy + filling[typ] = { + "columns": self.fill_channels, + "overlap": overlap, + "iterations": iterations, + } - self.df_electron = df_electron - self.df_pulse = df_pulse + self.df[typ] = df + self.metadata.update({"file_statistics": file_stats, "filling": filling}) def run( self, @@ -246,6 +225,8 @@ def run( ) -> None: """ Runs the buffer file creation process. + Does a schema check on the buffer files and creates them if they are missing. + Performs forward filling and splits the sector ID from the DLD time lazily. Args: h5_paths (List[Path]): List of paths to H5 files. @@ -254,12 +235,31 @@ def run( suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode.): """ - self.fp = BufferFilePaths(h5_paths, folder, suffix) if not force_recreate: - self._schema_check() + schema_set = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + self._schema_check(self.fp["electron"], schema_set) + schema_set = set( + get_channels( + self._config["channels"], + formats=["per_pulse", "per_train"], + index=True, + extend_aux=True, + ), + ) - {"electronId"} + self._schema_check(self.fp["timed"], schema_set) self._save_buffer_files(force_recreate, debug) self._fill_dataframes() + + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + self.df["electron"], meta = split_dld_time_from_sector_id( + self.df["electron"], + config=self._config, + ) + self.metadata.update(meta) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 3bb4b39b..1b7387f8 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -215,7 +215,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[f KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. """ try: - file_statistics = self.metadata["file_statistics"] + file_statistics = self.metadata["file_statistics"]["timed"] except Exception as exc: raise KeyError( "File statistics missing. Use 'read_dataframe' first.", @@ -335,8 +335,8 @@ def read_dataframe( suffix=detector, debug=debug, ) - df = bh.df_electron - df_timed = bh.df_pulse + df = bh.df["electron"] + df_timed = bh.df["timed"] if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 16935d16..78046e28 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -51,12 +51,12 @@ def get_channels( if formats: # If 'formats' is a list, check if all elements are valid. + err_msg = ( + "Invalid format. Please choose from 'per_electron', 'per_pulse', 'per_train', 'all'." + ) for format_ in formats: if format_ not in FORMATS + ["all"]: - raise ValueError( - "Invalid format. Please choose from 'per_electron', 'per_pulse',\ - 'per_train', 'all'.", - ) + raise ValueError(err_msg) # Get the available channels excluding 'pulseId'. available_channels = list(channel_dict.keys()) @@ -83,19 +83,18 @@ def get_channels( return channels -def get_dtypes(channels_dict: dict, formats: str | list[str], extend_aux: bool = False) -> dict: +def get_dtypes(channels_dict: dict, formats: str | list[str]) -> dict: """Returns a dictionary of channels and their corresponding data types. + Currently Auxiliary channels are not included in the dtype dictionary. Args: channels_dict (dict): The dictionary containing the channels. formats (str | list[str]): The desired format(s). - extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, - else includes 'dldAux'. Returns: dict: A dictionary of channels and their corresponding data types. """ - channels = get_channels(channels_dict, formats, extend_aux) + channels = get_channels(channel_dict=channels_dict, formats=formats) return { channel: channels_dict[channel].get("dtype") for channel in channels diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 3e20e599..2115a1a3 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -52,7 +52,7 @@ def __init__(self, config: dict) -> None: self.failed_files_error: list[str] = [] self.array_indices: list[list[slice]] = None self.raw_dir: str = None - self.parquet_dir: str = None + self.processed_dir: str = None def _initialize_dirs(self): """ @@ -65,14 +65,14 @@ def _initialize_dirs(self): # Parses to locate the raw beamtime directory from config file if ( "paths" in self._config["core"] - and self._config["core"]["paths"].get("data_raw_dir", "") - and self._config["core"]["paths"].get("data_parquet_dir", "") + and self._config["core"]["paths"].get("raw", "") + and self._config["core"]["paths"].get("processed", "") ): data_raw_dir = [ - Path(self._config["core"]["paths"].get("data_raw_dir", "")), + Path(self._config["core"]["paths"].get("raw", "")), ] data_parquet_dir = Path( - self._config["core"]["paths"].get("data_parquet_dir", ""), + self._config["core"]["paths"].get("processed", ""), ) else: @@ -100,7 +100,7 @@ def _initialize_dirs(self): data_parquet_dir.mkdir(parents=True, exist_ok=True) self.raw_dir = data_raw_dir - self.parquet_dir = data_parquet_dir + self.processed_dir = data_parquet_dir def get_files_from_run_id( self, @@ -970,7 +970,7 @@ def read_dataframe( metadata=metadata, ) - df, df_timed = self.parquet_handler(Path(self.parquet_dir), **kwds) + df, df_timed = self.parquet_handler(Path(self.processed_dir), **kwds) if collect_metadata: metadata = self.gather_metadata( diff --git a/tests/data/loader/sxp/config.yaml b/tests/data/loader/sxp/config.yaml index cc4b48e5..095178ff 100644 --- a/tests/data/loader/sxp/config.yaml +++ b/tests/data/loader/sxp/config.yaml @@ -2,8 +2,8 @@ core: loader: sxp beamline: sxp paths: - data_raw_dir: "tests/data/loader/sxp/" - data_parquet_dir: "tests/data/loader/sxp/parquet" + raw: "tests/data/loader/sxp/" + processed: "tests/data/loader/sxp/parquet" binning: num_cores: 10 diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index eb4d2c6f..6a83e398 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -7,6 +7,7 @@ import pytest from h5py import File +from sed.loader.flash.buffer_handler import BufferFilePaths from sed.loader.flash.buffer_handler import BufferHandler from sed.loader.flash.utils import get_channels @@ -23,55 +24,71 @@ def create_parquet_dir(config: dict, folder: str) -> Path: return parquet_path -def test_get_files_to_read(config: dict, h5_paths: list[Path]) -> None: +def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: """ - Test the BufferHandler's ability to identify files that need to be read and - manage buffer file paths. + Test the BufferFilePath's ability to identify files that need to be read and + manage buffer file paths using a directory structure. - This test performs several checks to ensure the BufferHandler correctly identifies + This test performs several checks to ensure the BufferFilePath correctly identifies which HDF5 files need to be read and properly manages the paths for saving buffer files. It follows these steps: 1. Creates a directory structure for storing buffer files and initializes the BufferHandler. - 2. Invokes the private method _get_files_to_read to populate the list of missing HDF5 files and + 2. Checks if the to_process method populates the dict of missing file sets and verify that initially, all provided files are considered missing. 3. Checks that the paths for saving buffer files are correctly generated. - 4. Creates a single buffer file and reruns the check to ensure that the BufferHandler recognizes - one less missing file. - 5. Cleans up by removing the created buffer file. - 6. Tests the handling of prefix and suffix in buffer file names by rerunning the checks with - modified file name parameters. + 4. Creates a single buffer file and reruns to_process to ensure that the BufferHandler + recognizes one less missing file. + 5. Checks if the force_recreate parameter forces the BufferHandler to consider all files + 6. Cleans up by removing the created buffer file. + 7. Tests the handling of suffix in buffer file names (for multidetector setups) by rerunning + the checks with modified file name parameters. """ folder = create_parquet_dir(config, "get_files_to_read") - subfolder = folder.joinpath("buffer") - # set to false to avoid creating buffer files unnecessarily - bh = BufferHandler(config) - bh._get_files_to_read(h5_paths, folder, "", "", False) + fp = BufferFilePaths(h5_paths, folder, "") # check that all files are to be read - assert np.all(bh.missing_h5_files == h5_paths) - + assert len(fp.to_process()) == len(h5_paths) + print(folder) # create expected paths - expected_buffer_paths = [Path(subfolder, f"{Path(path).stem}") for path in h5_paths] + expected_buffer_electron_paths = [ + folder / f"buffer/electron_{Path(path).stem}" for path in h5_paths + ] + expected_buffer_timed_paths = [folder / f"buffer/timed_{Path(path).stem}" for path in h5_paths] # check that all buffer paths are correct - assert np.all(bh.save_paths == expected_buffer_paths) + assert np.all(fp["electron"] == expected_buffer_electron_paths) + assert np.all(fp["timed"] == expected_buffer_timed_paths) + + # create a single buffer file to check if it changes + path = { + "raw": h5_paths[0], + "electron": expected_buffer_electron_paths[0], + "timed": expected_buffer_timed_paths[0], + } + bh = BufferHandler(config) + bh._save_buffer_file(path) - # create only one buffer file - bh._save_buffer_file(h5_paths[0], expected_buffer_paths[0]) - # check again for files to read - bh._get_files_to_read(h5_paths, folder, "", "", False) + # check again for files to read and expect one less file + fp = BufferFilePaths(h5_paths, folder, "") # check that only one file is to be read - assert len(bh.missing_h5_files) == len(h5_paths) - 1 - Path(expected_buffer_paths[0]).unlink() # remove buffer file + assert len(fp.to_process()) == len(h5_paths) - 1 + + # check that both files are to be read if force_recreate is set to True + assert len(fp.to_process(force_recreate=True)) == len(h5_paths) + + # remove buffer files + Path(path["electron"]).unlink() + Path(path["timed"]).unlink() - # add prefix and suffix - bh._get_files_to_read(h5_paths, folder, "prefix", "suffix", False) + # Test for adding a suffix + fp = BufferFilePaths(h5_paths, folder, "suffix") # expected buffer paths with prefix and suffix - expected_buffer_paths = [ - Path(subfolder, f"prefix_{Path(path).stem}_suffix") for path in h5_paths - ] - assert np.all(bh.save_paths == expected_buffer_paths) + for typ in ["electron", "timed"]: + expected_buffer_paths = [ + folder / "buffer" / f"{typ}_{Path(path).stem}_suffix" for path in h5_paths + ] + assert np.all(fp[typ] == expected_buffer_paths) def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: @@ -131,7 +148,9 @@ def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: assert "Missing in config: {'gmdTunnel2'}" in expected_error # Clean up created buffer files after the test - for path in bh.buffer_paths: + for path in bh.fp["electron"]: + path.unlink() + for path in bh.fp["timed"]: path.unlink() @@ -158,10 +177,11 @@ def test_save_buffer_files(config: dict, h5_paths: list[Path]) -> None: pd.testing.assert_frame_equal(df_serial, df_parallel) # remove buffer files - for path in bh_serial.buffer_paths: - path.unlink() - for path in bh_parallel.buffer_paths: - path.unlink() + for df_type in ["electron", "timed"]: + for path in bh_serial.fp[df_type]: + path.unlink() + for path in bh_parallel.fp[df_type]: + path.unlink() def test_save_buffer_files_exception( @@ -214,15 +234,18 @@ def test_get_filled_dataframe(config: dict, h5_paths: list[Path]) -> None: df = pd.read_parquet(folder) - assert np.all(list(bh.df_electron.columns) == list(df.columns) + ["dldSectorID"]) - - channel_pulse = get_channels( - config["dataframe"]["channels"], - formats=["per_pulse", "per_train"], - index=True, - extend_aux=True, - ) - assert np.all(list(bh.df_pulse.columns) == channel_pulse) + assert np.all(list(bh.df["electron"].columns) == list(df.columns) + ["dldSectorID"]) + + channel_pulse = set( + get_channels( + config["dataframe"]["channels"], + formats=["per_pulse", "per_train"], + index=True, + extend_aux=True, + ), + ) - {"electronId"} + assert np.all(set(bh.df["timed"].columns) == channel_pulse) # remove buffer files - for path in bh.buffer_paths: - path.unlink() + for df_type in ["electron", "timed"]: + for path in bh.fp[df_type]: + path.unlink() diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 14757341..be28e659 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -137,9 +137,11 @@ def test_get_elapsed_time_fid(config: dict) -> None: # Mock the file_statistics and files fl.metadata = { "file_statistics": { - "0": {"time_stamps": [10, 20]}, - "1": {"time_stamps": [20, 30]}, - "2": {"time_stamps": [30, 40]}, + "timed": { + "0": {"time_stamps": [10, 20]}, + "1": {"time_stamps": [20, 30]}, + "2": {"time_stamps": [30, 40]}, + }, }, } fl.files = ["file0", "file1", "file2"] @@ -163,8 +165,10 @@ def test_get_elapsed_time_fid(config: dict) -> None: # Test KeyError when time_stamps is missing fl.metadata = { "file_statistics": { - "0": {}, - "1": {"time_stamps": [20, 30]}, + "timed": { + "0": {}, + "1": {"time_stamps": [20, 30]}, + }, }, } with pytest.raises(KeyError) as e: @@ -174,12 +178,12 @@ def test_get_elapsed_time_fid(config: dict) -> None: def test_get_elapsed_time_run(config: dict) -> None: + """Test get_elapsed_time method of FlashLoader class""" config_ = config.copy() config_["core"]["paths"] = { "raw": "tests/data/loader/flash/", "processed": "tests/data/loader/flash/parquet/get_elapsed_time_run", } - """Test get_elapsed_time method of FlashLoader class""" config_ = config.copy() data_parquet_dir = create_parquet_dir(config_, "get_elapsed_time_run") config_["core"]["paths"]["data_parquet_dir"] = data_parquet_dir @@ -187,9 +191,9 @@ def test_get_elapsed_time_run(config: dict) -> None: fl = FlashLoader(config=config_) fl.read_dataframe(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"]["0"]["time_stamps"] + start, end = fl.metadata["file_statistics"]["electron"]["0"]["time_stamps"] expected_elapsed_time_0 = end - start - start, end = fl.metadata["file_statistics"]["1"]["time_stamps"] + start, end = fl.metadata["file_statistics"]["electron"]["1"]["time_stamps"] expected_elapsed_time_1 = end - start elapsed_time = fl.get_elapsed_time(runs=[43878]) @@ -199,12 +203,12 @@ def test_get_elapsed_time_run(config: dict) -> None: assert elapsed_time == [expected_elapsed_time_0, expected_elapsed_time_1] elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"]["1"]["time_stamps"] + start, end = fl.metadata["file_statistics"]["electron"]["1"]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 # remove the parquet files - for file in os.listdir(Path(fl.parquet_dir, "buffer")): - Path(fl.parquet_dir, "buffer").joinpath(file).unlink() + for file in os.listdir(Path(fl.processed_dir, "buffer")): + Path(fl.processed_dir, "buffer").joinpath(file).unlink() def test_available_runs(monkeypatch: pytest.MonkeyPatch, config: dict) -> None: diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 8a5d1507..0d194cea 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -95,8 +95,8 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["data_parquet_dir"] = ( - config["core"]["paths"]["data_parquet_dir"] + f"_{read_type}" + config["core"]["paths"]["processed"] = ( + config["core"]["paths"]["processed"] + f"_{read_type}" ) loader = get_loader(loader_name=loader.__name__, config=config) @@ -168,8 +168,8 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -183,8 +183,8 @@ def test_timed_dataframe(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["data_parquet_dir"] = ( - config["core"]["paths"]["data_parquet_dir"] + "_timed_dataframe" + config["core"]["paths"]["processed"] = ( + config["core"]["paths"]["processed"] + "_timed_dataframe" ) loader = get_loader(loader_name=loader.__name__, config=config) @@ -201,8 +201,8 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) pytest.skip("Not implemented") assert isinstance(loaded_timed_dataframe, ddf.DataFrame) assert set(loaded_timed_dataframe.columns).issubset(set(loaded_dataframe.columns)) @@ -211,8 +211,8 @@ def test_timed_dataframe(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -226,9 +226,7 @@ def test_get_count_rate(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["data_parquet_dir"] = ( - config["core"]["paths"]["data_parquet_dir"] + "_count_rate" - ) + config["core"]["paths"]["processed"] = config["core"]["paths"]["processed"] + "_count_rate" loader = get_loader(loader_name=loader.__name__, config=config) if loader.__name__ != "BaseLoader": @@ -245,8 +243,8 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) pytest.skip("Not implemented") assert len(loaded_time) == len(loaded_countrate) loaded_time2, loaded_countrate2 = loader.get_count_rate(fids=[0]) @@ -256,8 +254,8 @@ def test_get_count_rate(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) @pytest.mark.parametrize("loader", get_all_loaders()) @@ -271,8 +269,8 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["data_parquet_dir"] = ( - config["core"]["paths"]["data_parquet_dir"] + "_elapsed_time" + config["core"]["paths"]["processed"] = ( + config["core"]["paths"]["processed"] + "_elapsed_time" ) loader = get_loader(loader_name=loader.__name__, config=config) @@ -290,8 +288,8 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if loader.__name__ in {"sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) pytest.skip("Not implemented") assert elapsed_time > 0 elapsed_time2 = loader.get_elapsed_time(fids=[0]) @@ -301,8 +299,8 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() - for file in os.listdir(Path(loader.parquet_dir, "buffer")): - os.remove(Path(loader.parquet_dir, "buffer", file)) + for file in os.listdir(Path(loader.processed_dir, "buffer")): + os.remove(Path(loader.processed_dir, "buffer", file)) def test_mpes_timestamps() -> None: From 1b2e788ca9f19bd23eb00be98b4afcc47259a637 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jul 2024 20:05:24 +0200 Subject: [PATCH 151/300] sxp test fixes --- tests/loader/sxp/test_sxp_loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index cc8698a3..b31a1613 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -102,7 +102,7 @@ def test_initialize_dirs(config_file: dict, fs) -> None: sl._initialize_dirs() assert expected_raw_path == sl.raw_dir[0] - assert expected_processed_path == sl.parquet_dir + assert expected_processed_path == sl.processed_dir def test_initialize_dirs_filenotfound(config_file: dict): @@ -210,5 +210,5 @@ def test_buffer_schema_mismatch(config_file: dict): # Clean up created buffer files after the test sl._initialize_dirs() - for file in os.listdir(Path(sl.parquet_dir, "buffer")): - os.remove(Path(sl.parquet_dir, "buffer", file)) + for file in os.listdir(Path(sl.processed_dir, "buffer")): + os.remove(Path(sl.processed_dir, "buffer", file)) From 7ed0962e90528e8f439b67d9b8bd9ef91d8502f4 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jul 2024 20:06:31 +0200 Subject: [PATCH 152/300] processor test fixes --- tests/test_processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_processor.py b/tests/test_processor.py index f056f79f..8d71ffcb 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -628,8 +628,8 @@ def test_align_dld_sectors() -> None: user_config={}, system_config={}, ) - config["core"]["paths"]["data_parquet_dir"] = ( - config["core"]["paths"]["data_parquet_dir"] + "_align_dld_sectors" + config["core"]["paths"]["processed"] = ( + config["core"]["paths"]["processed"] + "_align_dld_sectors" ) processor = SedProcessor( folder=df_folder + "../flash/", @@ -670,7 +670,7 @@ def test_align_dld_sectors() -> None: np.testing.assert_allclose(tof_ref_array, tof_aligned_array + sector_delays[:, np.newaxis]) # cleanup flash intermediaries - parquet_data_dir = config["core"]["paths"]["data_parquet_dir"] + parquet_data_dir = config["core"]["paths"]["processed"] for file in os.listdir(Path(parquet_data_dir, "buffer")): os.remove(Path(parquet_data_dir, "buffer", file)) From 7d1262be48c67b2bfe883077831f9b099c90c928 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 7 Jul 2024 20:14:51 +0200 Subject: [PATCH 153/300] sxp test fixes --- tests/loader/sxp/test_sxp_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index b31a1613..09588152 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -150,7 +150,7 @@ def test_data_keys_not_in_h5(config_file: dict, key_type: str): sl = SXPLoader(config=config) with pytest.raises(ValueError) as e: - sl.create_dataframe_per_file(config["core"]["paths"]["data_raw_dir"] + H5_PATH) + sl.create_dataframe_per_file(config["core"]["paths"]["raw"] + H5_PATH) assert str(e.value.args[0]) == f"The {key_type} for channel dldPosX does not exist." From 638ed687eca04aaf9b27e9baef0cc3e89a47fb25 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 8 Jul 2024 12:39:14 +0200 Subject: [PATCH 154/300] add further tests --- sed/calibrator/energy.py | 4 ++-- tests/calibrator/test_energy.py | 23 +++++++++++++++++++++++ tests/calibrator/test_momentum.py | 16 ++++++++++++++++ tests/loader/test_loaders.py | 8 ++++++++ tests/test_diagnostics.py | 4 ++++ tests/test_processor.py | 12 ++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 5b51e834..2880667a 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -2279,7 +2279,7 @@ def poly_energy_calibration( - **'kinetic'**: increasing energy with decreasing TOF. - **'binding'**: increasing energy with increasing TOF. - **kwds: Keyword arguments passed to ``lsqr``. + **kwds: Keyword arguments passed to ``lstsq`` or ``lsqr``. Returns: dict: A dictionary of fitting parameters including the following, @@ -2317,7 +2317,7 @@ def poly_energy_calibration( # Solve for the a vector (polynomial coefficients) using least squares if method == "lstsq": - sol = lstsq(t_mat, bvec, rcond=None) + sol = lstsq(t_mat, bvec, rcond=None, **kwds) elif method == "lsqr": sol = lsqr(t_mat, bvec, **kwds) poly_a = sol[0] diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 670ecd82..336b6e78 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -131,6 +131,10 @@ def test_feature_extract() -> None: ((tof[1] - tof[0]) * np.asarray(rand) + 65000) + diff, ) + # illegal keywords + with pytest.raises(TypeError): + ec.add_ranges(ranges=rng, ref_id=ref_id, illegal_kwd=True) + def test_adjust_ranges() -> None: """Test the interactive function for adjusting the feature ranges""" @@ -163,6 +167,10 @@ def test_adjust_ranges() -> None: ((tof[1] - tof[0]) * np.asarray(rand) + 65000) + diff, ) + # illegal keywords + with pytest.raises(TypeError): + ec.adjust_ranges(ranges=rng, ref_id=ref_id, apply=True, illegal_kwd=True) + energy_scales = ["kinetic", "binding"] calibration_methods = ["lmfit", "lstsq", "lsqr"] @@ -216,6 +224,15 @@ def test_calibrate_append(energy_scale: str, calibration_method: str) -> None: value, ) + # illegal keywords + with pytest.raises(TypeError): + calibdict = ec.calibrate( + ref_energy=e_ref, + energy_scale=energy_scale, + method=calibration_method, + illegal_kwd=True, + ) + calib_types = ["fit", "poly"] calib_dicts = [{"d": 1, "t0": 0, "E0": 0}, {"coeffs": [1, 2, 3], "E0": 0}] @@ -300,6 +317,12 @@ def test_append_tof_ns_axis() -> None: assert config["dataframe"]["tof_ns_column"] in df.columns np.testing.assert_allclose(df[ec.tof_column], df[ec.tof_ns_column] / 2) + # illegal keywords: + df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) + ec = EnergyCalibrator(config=config, loader=loader) + with pytest.raises(TypeError): + df, _ = ec.append_tof_ns_axis(df, illegal_kwd=True) + amplitude = 2.5 # pylint: disable=invalid-name center = (730, 730) diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index 366c7d9d..de229e9f 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -69,6 +69,10 @@ def test_feature_extract() -> None: assert len(mc.pcent) == 2 assert len(mc.pouter_ord) == 6 + # illegal keywords + with pytest.raises(TypeError): + mc.feature_extract(features=np.ndarray([1, 2]), illegal_kwd=True) + @pytest.mark.parametrize( "include_center", @@ -169,6 +173,10 @@ def test_pose_correction() -> None: mc.pose_adjustment(scale=1.2, xtrans=8, ytrans=7, angle=-4, apply=True) assert np.all(np.array([mc.cdeform_field, mc.rdeform_field]) != dfield) + # Illegal keywords: + with pytest.raises(TypeError): + mc.reset_deformation(illegal_kwd=True) + def test_apply_correction() -> None: """Test the application of the distortion correction to the dataframe.""" @@ -352,6 +360,10 @@ def test_apply_registration( metadata["registration"]["center"], ) + # illegal keywords: + with pytest.raises(TypeError): + mc.pose_adjustment(illegal_kwd=True) + def test_momentum_calibration_equiscale() -> None: """Test the calibration using one point and the k-distance, @@ -385,6 +397,10 @@ def test_momentum_calibration_equiscale() -> None: for key, value in mc.calibration.items(): np.testing.assert_equal(metadata["calibration"][key], value) + # illegal keywords: + with pytest.raises(TypeError): + mc.append_k_axis(df, illegal_kwd=True) + def test_momentum_calibration_two_points() -> None: """Test the calibration using two k-points, and application to the dataframe.""" diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 8a5d1507..fadf411b 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -253,6 +253,10 @@ def test_get_count_rate(loader: BaseLoader) -> None: assert len(loaded_time2) == len(loaded_countrate2) assert len(loaded_time2) < len(loaded_time) + # illegal keywords + with pytest.raises(TypeError): + loader.get_count_rate(illegal_kwd=True) + if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() @@ -298,6 +302,10 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: assert elapsed_time2 > 0 assert elapsed_time > elapsed_time2 + # illegal keywords + with pytest.raises(TypeError): + loader.get_elapsed_time(illegal_kwd=True) + if loader.__name__ in {"flash", "sxp"}: loader = cast(FlashLoader, loader) loader._initialize_dirs() diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index d53ed8d9..2614e69d 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -42,3 +42,7 @@ def test_plot_histogram(ncols: int, backend: str) -> None: axes[loc] = config["dataframe"].get(axis.strip("@")) values = {axis: dataframe[axis].compute() for axis in axes} grid_histogram(values, ncols, axes, bins, ranges, backend) + + # illegal keywords: + with pytest.raises(TypeError): + grid_histogram(values, ncols, axes, bins, ranges, backend, illegal_kwd=True) diff --git a/tests/test_processor.py b/tests/test_processor.py index 43cd4815..1b9650d1 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -388,6 +388,10 @@ def test_pose_adjustment() -> None: assert "Xm" in processor.dataframe.columns assert "Ym" in processor.dataframe.columns + # illegal keywords: + with pytest.raises(TypeError): + processor.pose_adjustment(**adjust_params, apply=True, illegal_kwd=True) + def test_pose_adjustment_save_load() -> None: """Test for the saving and loading of pose correction and application of momentum correction @@ -889,6 +893,10 @@ def test_compute() -> None: assert result.data.shape == tuple(bins) assert result.data.sum(axis=(0, 1, 2, 3)) > 0 + # illegal keywords: + with pytest.raises(TypeError): + processor.compute(illegal_kwd=True) + def test_compute_with_filter() -> None: """Test binning of final result using filters""" @@ -1020,6 +1028,10 @@ def test_get_normalization_histogram() -> None: # histogram2 = processor.get_normalization_histogram(axis="ADC", use_time_stamps="True") # np.testing.assert_allclose(histogram1, histogram2) + # illegal keywords: + with pytest.raises(TypeError): + histogram1 = processor.get_normalization_histogram(axis="ADC", illegal_kwd=True) + metadata: dict[Any, Any] = {} metadata["entry_title"] = "Title" From 84f99efddc908de2a4eda7e59ba377cbc435e964 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 8 Jul 2024 12:51:34 +0200 Subject: [PATCH 155/300] fix typing --- tests/test_processor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_processor.py b/tests/test_processor.py index 1b9650d1..61f76880 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -283,7 +283,7 @@ def test_copy_tool() -> None: ) feature_list = [feature4, feature5, feature6, feature7] -adjust_params = { +adjust_params: dict[str, Any] = { "scale": np.random.randint(1, 10) / 10 + 0.5, "xtrans": np.random.randint(1, 50), "ytrans": np.random.randint(1, 50), @@ -359,11 +359,11 @@ def test_pose_adjustment() -> None: verbose=True, ) # pose adjustment w/o loaded image - processor.pose_adjustment(**adjust_params, use_correction=False, apply=True) # type: ignore + processor.pose_adjustment(**adjust_params, use_correction=False, apply=True) processor.bin_and_load_momentum_calibration(apply=True) # test pose adjustment - processor.pose_adjustment(**adjust_params, use_correction=False, apply=True) # type: ignore + processor.pose_adjustment(**adjust_params, use_correction=False, apply=True) processor = SedProcessor( folder=df_folder, @@ -383,7 +383,7 @@ def test_pose_adjustment() -> None: apply=True, ) processor.generate_splinewarp(use_center=True) - processor.pose_adjustment(**adjust_params, apply=True) # type: ignore[arg-type] + processor.pose_adjustment(**adjust_params, apply=True) processor.apply_momentum_correction() assert "Xm" in processor.dataframe.columns assert "Ym" in processor.dataframe.columns From 5b6f940d13abb0748f2b131a911b30bab1927802 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 8 Jul 2024 13:04:35 +0200 Subject: [PATCH 156/300] remove 0 vals from all pulse channels --- sed/loader/flash/dataframe.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 44548ea0..b5819374 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -221,8 +221,10 @@ def df_pulse(self) -> pd.DataFrame: ) # The dataset is opened and converted to numpy array by [()] # and flattened to resolve per pulse - # The pd.Series is created with the MultiIndex and appended to the list - series.append(pd.Series(dataset[()].ravel(), index=index, name=channel)) + channel_series = pd.Series(dataset[()].ravel(), index=index, name=channel) + # sometimes pulse columns have more pulses than valid ones such as with bam channel + # so we remove all 0 values from the series + series.append(channel_series[channel_series != 0]) # TODO: put this in metadata # All the channels are concatenated to a single DataFrame return pd.concat( From 1ef47b1cb0b6aa0b8107de29c3f3f1cc0ed9a5ca Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 8 Jul 2024 21:36:01 +0200 Subject: [PATCH 157/300] update energy calibration description --- ...pipeline_for_example_time-resolved_ARPES_data.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index dce9b1b7..34c448e1 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -458,9 +458,9 @@ "metadata": {}, "source": [ "#### 3. Step:\n", - "Next, the detected peak positions and bias voltages are used to determine the calibration function. This can be either done by fitting the functional form d^2/(t-t0)^2 via lmfit (\"lmfit\"), or using a polynomial approximation (\"lstsq\" or \"lsqr\"). Here, one can also define a reference id, and a reference energy. Those define the absolute energy position of the feature used for calibration in the \"reference\" trace, at the bias voltage where the final measurement has been performed. The energy scale can be either \"kinetic\" (decreasing energy with increasing TOF), or \"binding\" (increasing energy with increasing TOF).\n", + "Next, the detected peak positions and bias voltages are used to determine the calibration function. Essentially, the functional Energy(TOF) is being determined by either least-squares fitting of the functional form d^2/(t-t0)^2 via lmfit (``method``: \"lmfit\"), or by analytically obtaining a polynomial approximation (``method``: \"lstsq\" or \"lsqr\"). The parameter ``ref_energy`` is used to define the absolute energy position of the feature used for calibration in the calibrated energy scale. ``energy_scale`` can be either \"kinetic\" (decreasing energy with increasing TOF), or \"binding\" (increasing energy with increasing TOF).\n", "\n", - "After calculating the calibration, all traces corrected with the calibration are plotted ontop of each other, the calibration function together with the extracted features is plotted." + "After calculating the calibration, all traces corrected with the calibration are plotted ontop of each other, and the calibration function (Energy(TOF)) together with the extracted features is being plotted." ] }, { @@ -470,7 +470,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Eref can be used to set the absolute energy (kinetic energy, E-EF) of the feature used for energy calibration (if known)\n", + "# Eref can be used to set the absolute energy (kinetic energy, E-EF, etc.) of the feature used for energy calibration (if known)\n", "Eref=-1.3\n", "# the lmfit method uses a fit of (d/(t-t0))**2 to determine the energy calibration\n", "# limits and starting values for the fitting parameters can be provided as dictionaries\n", @@ -512,7 +512,7 @@ "metadata": {}, "source": [ "#### 4. Step:\n", - "Finally, the the energy axis is added to the dataframe." + "Finally, the the energy axis is added to the dataframe. Here, the applied bias voltages of the measurement is taken into account to provide the correct energy offset. If the bias cannot be read from the file, it can be provided manually." ] }, { @@ -668,7 +668,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.9.19" } }, "nbformat": 4, From a017fe40f5af5b81a2b2585228c6945789dd035c Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 8 Jul 2024 22:21:44 +0200 Subject: [PATCH 158/300] fix Energy(TOF) view for binding energy scale --- sed/core/processor.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sed/core/processor.py b/sed/core/processor.py index 3faf75e5..ae1b9082 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1345,13 +1345,13 @@ def calibrate_energy_axis( backend="bokeh", ) print("E/TOF relationship:") - self.ec.view( - traces=self.ec.calibration["axis"][None, :] + self.ec.biases[0], - xaxis=self.ec.tof, - backend="matplotlib", - show_legend=False, - ) if energy_scale == "kinetic": + self.ec.view( + traces=self.ec.calibration["axis"][None, :] + self.ec.biases[0], + xaxis=self.ec.tof, + backend="matplotlib", + show_legend=False, + ) plt.scatter( self.ec.peaks[:, 0], -(self.ec.biases - self.ec.biases[0]) + ref_energy, @@ -1359,6 +1359,12 @@ def calibrate_energy_axis( c="k", ) elif energy_scale == "binding": + self.ec.view( + traces=self.ec.calibration["axis"][None, :] - self.ec.biases[0], + xaxis=self.ec.tof, + backend="matplotlib", + show_legend=False, + ) plt.scatter( self.ec.peaks[:, 0], self.ec.biases - self.ec.biases[0] + ref_energy, From c0d61eae53dfba39b54fe685c76e71920a23142f Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 8 Jul 2024 23:55:26 +0200 Subject: [PATCH 159/300] add some more tests for mpes loader --- .cspell/custom-dictionary.txt | 1 + sed/loader/base/loader.py | 2 +- sed/loader/mpes/loader.py | 14 +++++ tests/loader/mpes/__init__.py | 0 tests/loader/mpes/test_mpes_loader.py | 79 +++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/loader/mpes/__init__.py create mode 100644 tests/loader/mpes/test_mpes_loader.py diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index dd0d304e..cfb563bb 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -37,6 +37,7 @@ bysource caldir calib calibdict +capsys cdeform cdeformfield cdisp diff --git a/sed/loader/base/loader.py b/sed/loader/base/loader.py index 2b4c4c20..4a9962d3 100644 --- a/sed/loader/base/loader.py +++ b/sed/loader/base/loader.py @@ -108,7 +108,7 @@ def read_dataframe( elif files is None: raise ValueError( - "Either folder, file paths, or runs should be provided!", + "Either folders, files, or runs have to be provided!", ) if files is not None: diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c24f94c8..3fd7136a 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -78,6 +78,13 @@ def hdf5_to_dataframe( f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", "Skipping the channel.", ) + elif channel["format"] != "per_file": + raise ValueError( + f"Invalid 'format':{channel['format']} for channel {name}.", + ) + + if not electron_channels: + raise ValueError("No valid 'per_electron' channels found.") if time_stamps: column_names.append(time_stamp_alias) @@ -181,6 +188,13 @@ def hdf5_to_timed_dataframe( f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", "Skipping the channel.", ) + elif channel["format"] != "per_file": + raise ValueError( + f"Invalid 'format':{channel['format']} for channel {name}.", + ) + + if not electron_channels: + raise ValueError("No valid 'per_electron' channels found.") if time_stamps: column_names.append(time_stamp_alias) diff --git a/tests/loader/mpes/__init__.py b/tests/loader/mpes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/loader/mpes/test_mpes_loader.py b/tests/loader/mpes/test_mpes_loader.py new file mode 100644 index 00000000..9d4513a0 --- /dev/null +++ b/tests/loader/mpes/test_mpes_loader.py @@ -0,0 +1,79 @@ +"""Tests specific for Mpes loader""" +from __future__ import annotations + +import os +from copy import deepcopy +from importlib.util import find_spec + +import pytest + +from sed.core.config import parse_config +from sed.loader.mpes.loader import MpesLoader + +package_dir = os.path.dirname(find_spec("sed").origin) + +test_data_dir = os.path.join(package_dir, "..", "tests", "data", "loader", "mpes") + +config = parse_config( + os.path.join(test_data_dir, "config.yaml"), + folder_config={}, + user_config={}, + system_config={}, +) + + +def test_channel_not_found_warning(capsys) -> None: + """Test if the mpes loader gives the correct warning if a channel cannot be found.""" + ml = MpesLoader(config=config) + + ml.read_dataframe(folders=test_data_dir) + captured = capsys.readouterr() + assert captured.out == "" + + # modify per_file channel + config_ = deepcopy(config) + config_["dataframe"]["channels"]["sampleBias"]["dataset_key"] = "invalid" + ml = MpesLoader(config=config_) + + ml.read_dataframe(folders=test_data_dir) + captured = capsys.readouterr() + assert 'Entry "invalid" for channel "sampleBias" not found.' in captured.out + + # modify per_electron channel + config_ = deepcopy(config) + config_["dataframe"]["channels"]["X"]["dataset_key"] = "invalid" + ml = MpesLoader(config=config_) + + ml.read_dataframe(folders=test_data_dir) + captured = capsys.readouterr() + assert 'Entry "invalid" for channel "X" not found.' in captured.out + + +def test_invalid_channel_format_raises() -> None: + """Test if the mpes loader raises an exception if an illegal channel format is provided.""" + config_ = deepcopy(config) + config_["dataframe"]["channels"]["sampleBias"]["format"] = "per_train" + ml = MpesLoader(config=config_) + + with pytest.raises(ValueError) as e: + ml.read_dataframe(folders=test_data_dir) + + expected_error = e.value.args[0] + + assert "Invalid 'format':per_train for channel sampleBias." in expected_error + + +def test_no_electron_channels_raises() -> None: + """Test if the mpes loader raises an exception if no per-electron channels are provided.""" + config_ = deepcopy(config) + config_["dataframe"]["channels"] = { + "sampleBias": {"format": "per_file", "dataset_key": "KTOF:Lens:Sample:V"}, + } + ml = MpesLoader(config=config_) + + with pytest.raises(ValueError) as e: + ml.read_dataframe(folders=test_data_dir) + + expected_error = e.value.args[0] + + assert "No valid 'per_electron' channels found." in expected_error From b325ba65dcca55e298acc362ee77bc69853cdd66 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 9 Jul 2024 09:18:30 +0200 Subject: [PATCH 160/300] update paths --- tutorial/4_hextof_workflow.ipynb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index f428bf71..27031e5b 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -99,8 +99,8 @@ "config_override = {\n", " \"core\": {\n", " \"paths\": {\n", - " \"data_raw_dir\": \"\",\n", - " \"data_parquet_dir\": \"\"\n", + " \"raw\": \"\",\n", + " \"processed\": \"\"\n", " },\n", " },\n", "}" @@ -119,13 +119,13 @@ "metadata": {}, "outputs": [], "source": [ - "config_override['core']['paths']['data_raw_dir'] = \"/asap3/flash/gpfs/pg2/2023/data/11019101/raw/hdf/offline/fl1user3\"\n", + "config_override['core']['paths']['raw'] = \"/asap3/flash/gpfs/pg2/2023/data/11019101/raw/hdf/offline/fl1user3\"\n", "# If this will not work for you, please change it to a path where you have write access\n", - "config_override['core']['paths']['data_parquet_dir'] = \"/asap3/flash/gpfs/pg2/2023/data/11019101/processed\"\n", + "config_override['core']['paths']['processed'] = \"/asap3/flash/gpfs/pg2/2023/data/11019101/processed\"\n", "# So we write to user space\n", - "config_override['core']['paths']['data_parquet_dir'] = path + \"/processed\"\n", + "config_override['core']['paths']['processed'] = path + \"/processed\"\n", "# If you aren't using maxwell and downloaded the data, use this path\n", - "config_override['core']['paths']['data_raw_dir'] = path" + "config_override['core']['paths']['raw'] = path" ] }, { From 852ce8e81d9a5fce420c092a12a83d320f6f3265 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 10 Jul 2024 19:34:17 +0200 Subject: [PATCH 161/300] revert the benchmarking --- benchmarks/benchmark_sed.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index 9ab5b003..3b633c30 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -54,7 +54,7 @@ def test_binning_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_1d"]: print(f"Updating targets for 'binning_1d' to {float(np.mean(result))}") @@ -78,7 +78,7 @@ def test_binning_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_4d"]: print(f"Updating targets for 'binning_4d' to {float(np.mean(result))}") @@ -103,7 +103,7 @@ def test_splinewarp() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin + assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["inv_dfield"]: print(f"Updating targets for 'inv_dfield' to {float(np.mean(result))}") @@ -137,7 +137,7 @@ def test_workflow_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_1d"]: print(f"Updating targets for 'workflow_1d' to {float(np.mean(result))}") @@ -171,7 +171,7 @@ def test_workflow_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_4d"]: print(f"Updating targets for 'workflow_4d' to {float(np.mean(result))}") @@ -188,7 +188,6 @@ def test_loader_compute(loader: BaseLoader) -> None: loaded_dataframe, _, loaded_metadata = loader.read_dataframe( runs=runs[loader_name], collect_metadata=False, - force_recreate=True, ) loaded_dataframe.compute() @@ -198,7 +197,7 @@ def test_loader_compute(loader: BaseLoader) -> None: ) result = timer.repeat(20, number=1) print(result) - assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin + assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets[f"loader_compute_{loader_name}"]: print( From 2de5243b812e032b0b14d065c6863854ac5a3857 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 15 Jul 2024 18:29:17 +0200 Subject: [PATCH 162/300] update aux channel handling --- poetry.lock | 551 ++++++++++++++------------- sed/config/flash_example_config.yaml | 34 +- sed/loader/flash/dataframe.py | 17 +- sed/loader/flash/utils.py | 4 +- tests/data/loader/flash/config.yaml | 29 +- 5 files changed, 342 insertions(+), 293 deletions(-) diff --git a/poetry.lock b/poetry.lock index e2b570d1..a6a10aec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -185,13 +185,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] [[package]] name = "asteval" -version = "1.0.0" +version = "1.0.1" description = "Safe, minimalistic evaluator of python expression using ast module" optional = false python-versions = ">=3.8" files = [ - {file = "asteval-1.0.0-py3-none-any.whl", hash = "sha256:71bf1ebd56da932f8d7decb3fa5cef37080b900e30be7e4647b3233033245d67"}, - {file = "asteval-1.0.0.tar.gz", hash = "sha256:03abb8c4057123e52c9052ff5745ffa0c7022aa73383e6143f2357098a1698d5"}, + {file = "asteval-1.0.1-py3-none-any.whl", hash = "sha256:c7992cc6d0487ab29700b4531d9e7870e4b2e16cf94603cd5b71b21503fb0cde"}, + {file = "asteval-1.0.1.tar.gz", hash = "sha256:377f4917e5b717bc525b887345231791945fe89c43660b7902a64e69827db87f"}, ] [package.extras] @@ -254,13 +254,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.6.24.0.31.11" +version = "0.2024.7.15.0.31.42" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" files = [ - {file = "astropy_iers_data-0.2024.6.24.0.31.11-py3-none-any.whl", hash = "sha256:0b3799034b0b76af8f915ef822d38cc90e00e235db0cb688018e4f567a8babb9"}, - {file = "astropy_iers_data-0.2024.6.24.0.31.11.tar.gz", hash = "sha256:ef0197b7b84dea248031e553687ea1dc58d7ac9473043693b2d33b46d81a9a12"}, + {file = "astropy_iers_data-0.2024.7.15.0.31.42-py3-none-any.whl", hash = "sha256:82fd179c37212ad6e1a717d77f14609bfd28863db50d0c325643b9e2d0c7f888"}, + {file = "astropy_iers_data-0.2024.7.15.0.31.42.tar.gz", hash = "sha256:703024d6cabe01281dff802fce97ae4ab8582bee2dc09c7ab8b05f2cbdc497a3"}, ] [package.extras] @@ -359,13 +359,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "bokeh" -version = "3.4.1" +version = "3.4.2" description = "Interactive plots and applications in the browser from Python" optional = false python-versions = ">=3.9" files = [ - {file = "bokeh-3.4.1-py3-none-any.whl", hash = "sha256:1e3c502a0a8205338fc74dadbfa321f8a0965441b39501e36796a47b4017b642"}, - {file = "bokeh-3.4.1.tar.gz", hash = "sha256:d824961e4265367b0750ce58b07e564ad0b83ca64b335521cd3421e9b9f10d89"}, + {file = "bokeh-3.4.2-py3-none-any.whl", hash = "sha256:931a43ee59dbf1720383ab904f8205e126b85561aac55592415b800c96f1b0eb"}, + {file = "bokeh-3.4.2.tar.gz", hash = "sha256:a16d5cc0abb93d2d270d70fc35851f3e1b9208814a985a4678e0ba5ef2d9cd42"}, ] [package.dependencies] @@ -713,63 +713,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.5.4" +version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, - {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, - {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, - {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, - {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, - {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, - {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, - {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, - {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, - {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, - {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, - {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, - {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, - {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, ] [package.dependencies] @@ -912,15 +912,26 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "entrypoints" +version = "0.4" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, +] + [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -1193,13 +1204,13 @@ test = ["blosc2 (>=2.5.1)", "blosc2-grok (>=0.2.2)"] [[package]] name = "identify" -version = "2.5.36" +version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -1518,13 +1529,13 @@ files = [ [[package]] name = "jsonschema" -version = "4.22.0" +version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, ] [package.dependencies] @@ -1539,11 +1550,11 @@ rfc3339-validator = {version = "*", optional = true, markers = "extra == \"forma rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} rpds-py = ">=0.7.1" uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format-nongpl\""} [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-specifications" @@ -1581,26 +1592,27 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "8.6.2" +version = "7.4.9" description = "Jupyter protocol implementation and client libraries" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, + {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"}, + {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +entrypoints = "*" +jupyter-core = ">=4.9.2" +nest-asyncio = ">=1.5.4" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" tornado = ">=6.2" -traitlets = ">=5.3" +traitlets = "*" [package.extras] -docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] +doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] [[package]] name = "jupyter-console" @@ -2307,40 +2319,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.0" +version = "3.9.1" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, - {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, - {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, - {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, - {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, - {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, - {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, - {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, - {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, - {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, - {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, - {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, - {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, - {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, - {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, - {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, - {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, - {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, - {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, ] [package.dependencies] @@ -2687,13 +2699,13 @@ files = [ [[package]] name = "notebook" -version = "6.5.4" +version = "6.5.7" description = "A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.7" files = [ - {file = "notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f"}, - {file = "notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e"}, + {file = "notebook-6.5.7-py3-none-any.whl", hash = "sha256:a6afa9a4ff4d149a0771ff8b8c881a7a73b3835f9add0606696d6e9d98ac1cd0"}, + {file = "notebook-6.5.7.tar.gz", hash = "sha256:04eb9011dfac634fbd4442adaf0a8c27cd26beef831fe1d19faf930c327768e4"}, ] [package.dependencies] @@ -2701,7 +2713,7 @@ argon2-cffi = "*" ipykernel = "*" ipython-genutils = "*" jinja2 = "*" -jupyter-client = ">=5.3.4" +jupyter-client = ">=5.3.4,<8" jupyter-core = ">=4.6.1" nbclassic = ">=0.4.7" nbconvert = ">=5" @@ -2833,14 +2845,12 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] [[package]] @@ -2922,7 +2932,6 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -2936,14 +2945,12 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -2954,9 +2961,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3407,13 +3414,13 @@ test = ["pytest", "pytest-doctestplus (>=0.7)"] [[package]] name = "pyfakefs" -version = "5.5.0" +version = "5.6.0" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ - {file = "pyfakefs-5.5.0-py3-none-any.whl", hash = "sha256:8dbf203ab7bef1529f11f7d41b9478b898e95bf9f3b71262163aac07a518cd76"}, - {file = "pyfakefs-5.5.0.tar.gz", hash = "sha256:7448aaa07142f892d0a4eb52a5ed3206a9f02c6599e686cd97d624c18979c154"}, + {file = "pyfakefs-5.6.0-py3-none-any.whl", hash = "sha256:1a45bba8615323ec29d65929d32dc66d7b59a1e60a02109950440edb0486c539"}, + {file = "pyfakefs-5.6.0.tar.gz", hash = "sha256:7a549b32865aa97d8ba6538285a93816941d9b7359be2954ac60ec36b277e879"}, ] [[package]] @@ -3432,13 +3439,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.4.0" +version = "0.5.0" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.4.0-py3-none-any.whl", hash = "sha256:eb9d4cd74ec586edf420d854da50b65c743275efa1a560fb59d988ba68160c9d"}, - {file = "pynxtools-0.4.0.tar.gz", hash = "sha256:a36457177783f2ceb3daeb81cc51de203b5bc79235b0e2193e9cb6a67f6e3073"}, + {file = "pynxtools-0.5.0-py3-none-any.whl", hash = "sha256:4deb5d5f5608d468669182ca5cceb417efa8a6a3c9e40eb3d280bf00a1d6c6a3"}, + {file = "pynxtools-0.5.0.tar.gz", hash = "sha256:0ed009f40f14642a14f6783cf00874d803fa2508c2748e380493cbd875921e59"}, ] [package.dependencies] @@ -3450,37 +3457,38 @@ h5py = ">=3.6.0" importlib-metadata = "*" lxml = ">=4.9.1" mergedeep = "*" -numpy = ">=1.21.2,<2.0.0" +numpy = ">=1.21.2" pandas = ">=1.3.2" PyYAML = ">=6.0" xarray = ">=0.20.2" [package.extras] apm = ["pynxtools-apm"] -convert = ["pynxtools[apm,ellips,em,mpes,stm,xps,xrd]"] -dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (==0.3.4)", "structlog", "types-pytz", "types-pyyaml", "types-requests", "uv"] +convert = ["pynxtools[apm,ellips,em,mpes,raman,stm,xps,xrd]"] +dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (==0.4.8)", "structlog", "types-pytz", "types-pyyaml", "types-requests", "uv"] docs = ["mkdocs", "mkdocs-click", "mkdocs-macros-plugin", "mkdocs-material", "mkdocs-material-extensions"] ellips = ["pynxtools-ellips"] em = ["pynxtools-em"] mpes = ["pynxtools-mpes"] +raman = ["pynxtools-raman"] stm = ["pynxtools-stm"] xps = ["pynxtools-xps"] xrd = ["pynxtools-xrd"] [[package]] name = "pynxtools-mpes" -version = "0.1.0" +version = "0.1.1" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools_mpes-0.1.0-py3-none-any.whl", hash = "sha256:7632a5473b54435210605e330b0ffb014a57169594a20440cd88fc1667d3d407"}, - {file = "pynxtools_mpes-0.1.0.tar.gz", hash = "sha256:0ea950a5d84f25e1437ceb86c70e13b380a9f1b77b3cdcfc44b4ee569ee5232d"}, + {file = "pynxtools_mpes-0.1.1-py3-none-any.whl", hash = "sha256:44e5d1881d9e36cd5977831e4edf7cc96ef1dc595e0725119f255986cc5f658b"}, + {file = "pynxtools_mpes-0.1.1.tar.gz", hash = "sha256:e8f2eed853ebc7e28d24ba077994f6a3dd1848a3a3dcc7cbd1e115ff76ee7e46"}, ] [package.dependencies] h5py = ">=3.6.0" -pynxtools = ">=0.3.2" +pynxtools = ">=0.4.0" PyYAML = ">=6.0" xarray = ">=0.20.2" @@ -3675,7 +3683,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3966,110 +3973,110 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.18.1" +version = "0.19.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, - {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, - {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, - {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, - {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, - {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, - {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, - {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, - {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, - {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, - {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, - {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, - {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582"}, + {file = "rpds_py-0.19.0-cp310-none-win32.whl", hash = "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336"}, + {file = "rpds_py-0.19.0-cp310-none-win_amd64.whl", hash = "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952"}, + {file = "rpds_py-0.19.0-cp311-none-win32.whl", hash = "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf"}, + {file = "rpds_py-0.19.0-cp311-none-win_amd64.whl", hash = "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248"}, + {file = "rpds_py-0.19.0-cp312-none-win32.whl", hash = "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600"}, + {file = "rpds_py-0.19.0-cp312-none-win_amd64.whl", hash = "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae"}, + {file = "rpds_py-0.19.0-cp38-none-win32.whl", hash = "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4"}, + {file = "rpds_py-0.19.0-cp38-none-win_amd64.whl", hash = "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be"}, + {file = "rpds_py-0.19.0-cp39-none-win32.whl", hash = "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb"}, + {file = "rpds_py-0.19.0-cp39-none-win_amd64.whl", hash = "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521"}, + {file = "rpds_py-0.19.0.tar.gz", hash = "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9"}, ] [[package]] @@ -4206,18 +4213,18 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "70.1.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -4265,27 +4272,27 @@ files = [ [[package]] name = "sphinx" -version = "7.3.7" +version = "7.4.3" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ - {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, - {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, + {file = "sphinx-7.4.3-py3-none-any.whl", hash = "sha256:a3c295d0e8be6277e0a5ba5c6909a308bd208876b0f4f68c7cc591f566129412"}, + {file = "sphinx-7.4.3.tar.gz", hash = "sha256:bd846bcb09fd2b6e94ce3e1ad50f4618bccf03cc7c17d0f3fa87393c0bd9178b"}, ] [package.dependencies] alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.22" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.14" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" @@ -4296,8 +4303,8 @@ tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-autodoc-typehints" @@ -4521,13 +4528,13 @@ files = [ [[package]] name = "tifffile" -version = "2024.6.18" +version = "2024.7.2" description = "Read and write TIFF files" optional = false python-versions = ">=3.9" files = [ - {file = "tifffile-2024.6.18-py3-none-any.whl", hash = "sha256:67299c0445fc47463bbc71f3cb4676da2ab0242b0c6c6542a0680801b4b97d8a"}, - {file = "tifffile-2024.6.18.tar.gz", hash = "sha256:57e0d2a034bcb6287ea3155d8716508dfac86443a257f6502b57ee7f8a33b3b6"}, + {file = "tifffile-2024.7.2-py3-none-any.whl", hash = "sha256:5a2ee608c9cc1f2e044d943dacebddc71d4827b6fad150ef4c644b7aefbe2d1a"}, + {file = "tifffile-2024.7.2.tar.gz", hash = "sha256:02e52e8872c0e9943add686d2fd8bcfb18f0a824760882cf5e35fcbc2c80e32c"}, ] [package.dependencies] @@ -4567,13 +4574,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.5" +version = "0.13.0" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, ] [[package]] @@ -4666,13 +4673,13 @@ files = [ [[package]] name = "types-requests" -version = "2.32.0.20240622" +version = "2.32.0.20240712" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"}, - {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"}, + {file = "types-requests-2.32.0.20240712.tar.gz", hash = "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358"}, + {file = "types_requests-2.32.0.20240712-py3-none-any.whl", hash = "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"}, ] [package.dependencies] diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 357cff16..25ed436c 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -118,6 +118,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 2 + dtype: uint16 # detector x position dldPosX: @@ -125,6 +126,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 1 + dtype: uint16 # detector y position dldPosY: @@ -132,6 +134,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 0 + dtype: uint16 # Detector time-of-flight channel # if split_sector_id_from_dld_time is set to True, This this will generate @@ -141,6 +144,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 3 + dtype: uint32 # The auxiliary channel has a special structure where the group further contains # a multidimensional structure so further aliases are defined below @@ -149,14 +153,28 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - dldAuxChannels: - sampleBias: 0 - tofVoltage: 1 - extractorVoltage: 2 - extractorCurrent: 3 - cryoTemperature: 4 - sampleTemperature: 5 - dldTimeBinSize: 15 + aux_channels: + sampleBias: + slice: 0 + dtype: float64 + tofVoltage: + slice: 1 + dtype: float64 + extractorVoltage: + slice: 2 + dtype: float64 + extractorCurrent: + slice: 3 + dtype: float64 + cryoTemperature: + slice: 4 + dtype: float64 + sampleTemperature: + slice: 5 + dtype: float64 + dldTimeBinSize: + slice: 15 + dtype: float64 # ADC containing the pulser sign (1: value approx. 35000, 0: 33000) pulserSignAdc: diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index b5819374..687abe84 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -125,7 +125,9 @@ def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: # the number of electrons in each pulse. Here the values are counted electron_counts = pulse_index.value_counts(sort=False).values # Now we resolve each pulse to its electrons - electron_index = np.concatenate([np.arange(count) for count in electron_counts]) + electron_index = np.concatenate( + [np.arange(count, dtype="uint16") for count in electron_counts], + ) # Final multi-index constructed here index = pd.MultiIndex.from_arrays( @@ -258,9 +260,16 @@ def df_train(self) -> pd.DataFrame: # they come in pulse format, so the extra values are sliced and individual channels are # created and appended to the list if channel == "dldAux": - aux_channels = self._config["channels"]["dldAux"]["dldAuxChannels"].items() - for name, slice_aux in aux_channels: - series.append(pd.Series(dataset[: key.size, slice_aux], index, name=name)) + aux_channels = self._config["channels"]["dldAux"]["aux_channels"].items() + for name, values in aux_channels: + series.append( + pd.Series( + dataset[: key.size, values["slice"]], + index, + name=name, + dtype=values["dtype"], + ), + ) else: series.append(pd.Series(dataset, index, name=channel)) # All the channels are concatenated to a single DataFrame diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 78046e28..c4858686 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -5,7 +5,7 @@ MULTI_INDEX = ["trainId", "pulseId", "electronId"] PULSE_ALIAS = MULTI_INDEX[1] DLD_AUX_ALIAS = "dldAux" -DLDAUX_CHANNELS = "dldAuxChannels" +AUX_CHANNELS = "aux_channels" FORMATS = ["per_electron", "per_pulse", "per_train"] @@ -75,7 +75,7 @@ def get_channels( if format_ == FORMATS[2] and DLD_AUX_ALIAS in available_channels: if extend_aux: channels.extend( - channel_dict[DLD_AUX_ALIAS][DLDAUX_CHANNELS].keys(), + channel_dict[DLD_AUX_ALIAS][AUX_CHANNELS].keys(), ) else: channels.extend([DLD_AUX_ALIAS]) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 0a9a8170..ab4a310c 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -85,6 +85,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 2 + dtype: uint16 dldPosX: format: per_electron @@ -115,13 +116,27 @@ dataframe: dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 dldAuxChannels: - sampleBias: 0 - tofVoltage: 1 - extractorVoltage: 2 - extractorCurrent: 3 - cryoTemperature: 4 - sampleTemperature: 5 - dldTimeBinSize: 15 + sampleBias: + slice: 0 + dtype: float64 + tofVoltage: + slice: 1 + dtype: float64 + extractorVoltage: + slice: 2 + dtype: float64 + extractorCurrent: + slice: 3 + dtype: float64 + cryoTemperature: + slice: 4 + dtype: float64 + sampleTemperature: + slice: 5 + dtype: float64 + dldTimeBinSize: + slice: 15 + dtype: float64 timeStamp: format: per_train From cce0ba76ceeb64136616906fa390dbb73a10edd3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 17 Jul 2024 01:00:52 +0200 Subject: [PATCH 163/300] limit matplotlib to fix typing issue --- poetry.lock | 62 +++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/poetry.lock b/poetry.lock index 902c8bea..fbb2b675 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2307,40 +2307,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.1" +version = "3.9.0" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, - {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, - {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, - {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, - {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, - {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, - {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, - {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, - {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, - {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, - {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, + {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, + {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, + {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, + {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, + {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, + {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, + {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, + {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, + {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, ] [package.dependencies] @@ -4980,4 +4980,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "0d4830168219b4ad9cc4787d1f432fd1975f2bad131963d0f83017a63fb36d0e" +content-hash = "c477665d8b459fa3fc5510dde937d95401c1907de0a5cf831101999b74c99f3d" diff --git a/pyproject.toml b/pyproject.toml index 3d0a7424..aca89c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ h5py = ">=3.6.0" ipympl = ">=0.9.1" ipywidgets = "^7.7.1" lmfit = ">=1.0.3" -matplotlib = ">=3.5.1" +matplotlib = ">=3.5.1, <3.9.1" natsort = ">=8.1.0" numba = ">=0.55.1" numpy = ">=1.18, <2.0" From 770af0da41ea8cd201b0abaee0e5aac4904c9b4c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 17 Jul 2024 16:35:52 +0200 Subject: [PATCH 164/300] index sorting --- sed/config/flash_example_config.yaml | 2 +- sed/loader/flash/buffer_handler.py | 190 ++++++++++++++------------- sed/loader/flash/dataframe.py | 10 +- sed/loader/flash/loader.py | 4 +- sed/loader/flash/utils.py | 20 ++- sed/loader/utils.py | 37 +++--- 6 files changed, 139 insertions(+), 124 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 25ed436c..a09dd9f2 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -156,7 +156,7 @@ dataframe: aux_channels: sampleBias: slice: 0 - dtype: float64 + dtype: float32 tofVoltage: slice: 1 dtype: float64 diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 0d92e201..dac1df41 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -21,15 +21,14 @@ class BufferFilePaths: """ - A class for handling the paths to the raw and buffer files of electron and timed dataframes. + A class for handling the paths to the raw and buffer files. A list of file sets (dict) are created for each H5 file containing the paths to the raw file and the electron and timed buffer files. Structure of the file sets: { "raw": Path to the H5 file, - "electron": Path to the electron buffer file, - "timed": Path to the timed buffer file, + "buffer": Path to the buffer file } """ @@ -49,7 +48,7 @@ def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: self._file_paths = [ { "raw": h5_path, - **{typ: folder / f"{typ}_{h5_path.stem}{suffix}" for typ in DF_TYP}, + "buffer": folder / h5_path.stem, } for h5_path in h5_paths ] @@ -65,14 +64,11 @@ def __iter__(self): def __len__(self): return len(self._file_paths) - def to_process(self, force_recreate: bool = False) -> list[dict[str, Path]]: + def file_sets_to_process(self, force_recreate: bool = False) -> list[dict[str, Path]]: """Returns a list of file sets that need to be processed.""" - if not force_recreate: - return [ - file_set for file_set in self if any(not file_set[key].exists() for key in DF_TYP) - ] - else: - return list(self) + if force_recreate: + return self._file_paths + return [file_set for file_set in self._file_paths if not file_set["buffer"].exists()] class BufferHandler: @@ -101,14 +97,17 @@ def __init__( ) self.metadata: dict = {} - def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: + def _schema_check(self) -> None: """ Checks the schema of the Parquet files. Raises: ValueError: If the schema of the Parquet files does not match the configuration. """ - existing = [file for file in files if file.exists()] + expected_schema_set = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + existing = [file for file in self.fp["buffer"] if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing] for filename, schema in zip(existing, parquet_schemas): @@ -129,37 +128,20 @@ def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: "Please check the configuration file or set force_recreate to True.", ) - def _save_buffer_file(self, paths: dict[str, Path]) -> None: + def _save_buffer_file(self, path: dict[str, Path]) -> None: """ Creates the electron and timed buffer files from the raw H5 file. - First the dataframe is accessed and forward filled in the non-electron channels. - Then the data types are set. For the electron dataframe, all values not in the electron - channels are dropped. For the timed dataframe, only the train and pulse channels are taken - and it pulse resolved (no longer electron resolved). Both are saved as parquet files. + Args: paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. """ # Create a DataFrameCreator instance and the h5 file - df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df - - # forward fill all the non-electron channels - df[self.fill_channels] = df[self.fill_channels].ffill() - - # Reset the index of the DataFrame and save both the electron and timed dataframes - # electron resolved dataframe - electron_channels = get_channels(self._config["channels"], "per_electron") - dtypes = get_dtypes(self._config["channels"], "all") - df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( - paths["electron"], + DataFrameCreator(config_dataframe=self._config, h5_path=path["raw"]).df.to_parquet( + path["buffer"], ) - # timed dataframe - dtypes = get_dtypes(self._config["channels"], ["per_pulse", "per_train"]) - # drop the electron channels and only take rows with the first electronId - df[self.fill_channels].loc[:, :, 0].astype(dtypes).reset_index().to_parquet(paths["timed"]) - def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ Creates the buffer files that are missing. @@ -168,52 +150,97 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: force_recreate (bool): Flag to force recreation of buffer files. debug (bool): Flag to enable debug mode, which serializes the creation. """ - to_process = self.fp.to_process(force_recreate) - print(f"Reading files: {len(to_process)} new files of {len(self.fp)} total.") - n_cores = min(len(to_process), self.n_cores) + file_sets = self.fp.file_sets_to_process(force_recreate) + print(f"Reading files: {len(file_sets)} new files of {len(self.fp)} total.") + n_cores = min(len(file_sets), self.n_cores) if n_cores > 0: if debug: - for file_set in to_process: + for file_set in file_sets: self._save_buffer_file(file_set) else: Parallel(n_jobs=n_cores, verbose=10)( - delayed(self._save_buffer_file)(file_set) for file_set in to_process + delayed(self._save_buffer_file)(file_set) for file_set in file_sets ) - def _fill_dataframes(self): + def _fill_dataframes(self, df): """ - Reads all parquet files into one dataframe using dask and fills NaN values. + Reads all parquet files into one dataframe using dask and fills NaN values lazily. """ - # Loop over the electron and timed dataframes - file_stats = {} - filling = {} - for typ in DF_TYP: - # Read the parquet files into a dask dataframe - df = dd.read_parquet(self.fp[typ], calculate_divisions=True) - # Get the metadata from the parquet files - file_stats[typ] = get_parquet_metadata( - self.fp[typ], - time_stamp_col=self._config.get("time_stamp_alias", "timeStamp"), + # Forward fill the non-electron channels across files + overlap = min(file["num_rows"] for file in self.metadata["file_statistics"].values()) + iterations = self._config.get("forward_fill_iterations", 2) + df = forward_fill_lazy( + df=df, + columns=self.fill_channels, + before=overlap, + iterations=iterations, + ) + # TODO: This dict should be returned by forward_fill_lazy + filling = { + "columns": self.fill_channels, + "overlap": overlap, + "iterations": iterations, + } + self.metadata.update({"filling": filling}) + return df + + def _get_dataframes(self) -> None: + """ + Reads the buffer files from a folder. + + First the buffer files are read as a dask dataframe is accessed. + The dataframe is forward filled lazily with non-electron channels. + For the electron dataframe, all values not in the electron channels + are dropped, and splits the sector ID from the DLD time. + For the timed dataframe, only the train and pulse channels are taken and + it pulse resolved (no longer electron resolved). If time_index is True, + the timeIndex is calculated and set as the index (slow operation). + """ + df = dd.read_parquet(self.fp["buffer"], calculate_divisions=True) + # Get the metadata from the parquet files + file_stats = get_parquet_metadata( + self.fp["buffer"], + ) + self.metadata.update({"file_statistics": file_stats}) + df = self._fill_dataframes(df) + + # Electron dataframe + electron_dtypes = get_dtypes(self._config["channels"], "per_electron") + self.df["electron"] = df.dropna( + subset=get_channels(self._config["channels"], "per_electron"), + ).astype(electron_dtypes) + # Correct the 3-bit shift which encodes the detector ID in the 8s time + if self._config.get("split_sector_id_from_dld_time", False): + self.df["electron"], meta = split_dld_time_from_sector_id( + self.df["electron"], + config=self._config, ) + self.metadata.update(meta) - # Forward fill the non-electron channels across files - overlap = min(file["num_rows"] for file in file_stats[typ].values()) - iterations = self._config.get("forward_fill_iterations", 2) - df = forward_fill_lazy( - df=df, - columns=self.fill_channels, - before=overlap, - iterations=iterations, - ) - # TODO: This dict should be returned by forward_fill_lazy - filling[typ] = { - "columns": self.fill_channels, - "overlap": overlap, - "iterations": iterations, - } + # Timed dataframe + df_timed = df[df["electronId"] == 0] + timed_channels = get_channels( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + ["pulseId"] + df_timed = df_timed[timed_channels].dropna() + timed_dtypes = get_dtypes( + self._config["channels"], + ["per_pulse", "per_train"], + extend_aux=True, + ) + self.df["timed"] = df_timed.astype(timed_dtypes) - self.df[typ] = df - self.metadata.update({"file_statistics": file_stats, "filling": filling}) + if self._config.get("time_index", False): + # Calculate the time delta for each pulseId (1 microsecond per pulse) + df_timed["timeIndex"] = dd.to_datetime( + df_timed["timeStamp"], + unit="s", + ) + dd.to_timedelta(df_timed["pulseId"], unit="us") + + # Set the new fine timeStamp as the index if needed + self.df["timed"] = df_timed.set_index("timeIndex") def run( self, @@ -225,8 +252,6 @@ def run( ) -> None: """ Runs the buffer file creation process. - Does a schema check on the buffer files and creates them if they are missing. - Performs forward filling and splits the sector ID from the DLD time lazily. Args: h5_paths (List[Path]): List of paths to H5 files. @@ -237,29 +262,8 @@ def run( """ self.fp = BufferFilePaths(h5_paths, folder, suffix) - if not force_recreate: - schema_set = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) - self._schema_check(self.fp["electron"], schema_set) - schema_set = set( - get_channels( - self._config["channels"], - formats=["per_pulse", "per_train"], - index=True, - extend_aux=True, - ), - ) - {"electronId"} - self._schema_check(self.fp["timed"], schema_set) + self._schema_check() self._save_buffer_files(force_recreate, debug) - self._fill_dataframes() - - # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): - self.df["electron"], meta = split_dld_time_from_sector_id( - self.df["electron"], - config=self._config, - ) - self.metadata.update(meta) + self._get_dataframes() diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 687abe84..a46e03ed 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -302,4 +302,12 @@ def df(self) -> pd.DataFrame: self.validate_channel_keys() # been tested with merge, join and concat # concat offers best performance, almost 3 times faster - return pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() + dataframe = ( + pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1) + .sort_index() + .reset_index() + ) + # we can set dtype to uint16 for index pulseId and electronId + dataframe["pulseId"] = dataframe["pulseId"].astype("uint16") + dataframe["electronId"] = dataframe["electronId"].astype("uint16") + return dataframe.set_index("trainId") diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 1b7387f8..61eab4f5 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -215,7 +215,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[f KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. """ try: - file_statistics = self.metadata["file_statistics"]["timed"] + file_statistics = self.metadata["file_statistics"] except Exception as exc: raise KeyError( "File statistics missing. Use 'read_dataframe' first.", @@ -224,7 +224,7 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[f def get_elapsed_time_from_fid(fid): try: fid = str(fid) # Ensure the key is a string - time_stamps = file_statistics[fid]["time_stamps"] + time_stamps = file_statistics[fid]["columns"]["timeStamp"] elapsed_time = max(time_stamps) - min(time_stamps) except KeyError as exc: raise KeyError( diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index c4858686..1ff9a274 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -83,20 +83,26 @@ def get_channels( return channels -def get_dtypes(channels_dict: dict, formats: str | list[str]) -> dict: +def get_dtypes(channels_dict: dict, formats: str | list[str], extend_aux: bool = False) -> dict: """Returns a dictionary of channels and their corresponding data types. Currently Auxiliary channels are not included in the dtype dictionary. Args: channels_dict (dict): The dictionary containing the channels. formats (str | list[str]): The desired format(s). + extend_aux (bool): If True, includes auxiliary channels. Returns: dict: A dictionary of channels and their corresponding data types. """ - channels = get_channels(channel_dict=channels_dict, formats=formats) - return { - channel: channels_dict[channel].get("dtype") - for channel in channels - if channels_dict[channel].get("dtype") is not None - } + channels = get_channels(channel_dict=channels_dict, formats=formats, extend_aux=extend_aux) + dtypes = {} + for channel in channels: + try: + dtypes[channel] = channels_dict[channel].get("dtype") + except KeyError: + try: + dtypes[channel] = channels_dict["dldAux"][channel].get("dtype") + except KeyError: + dtypes[channel] = None + return dtypes diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 2dff23d9..5d7c015e 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -206,37 +206,38 @@ def split_dld_time_from_sector_id( return df, {"split_dld_time_from_sector_id": metadata} -def get_timestamp_stats(meta: pq.FileMetaData, time_stamp_col: str) -> tuple[int, int]: +def get_stats(meta: pq.FileMetaData) -> dict[str, list[int]]: """ - Extracts the minimum and maximum timestamps from the metadata of a Parquet file. + Extracts the minimum and maximum of all columns from the metadata of a Parquet file. Args: meta (pq.FileMetaData): The metadata of the Parquet file. - time_stamp_col (str): The name of the column containing the timestamps. Returns: Tuple[int, int]: The minimum and maximum timestamps. """ - idx = meta.schema.names.index(time_stamp_col) - timestamps = [] - for i in range(meta.num_row_groups): - stats = meta.row_group(i).column(idx).statistics - timestamps.append(stats.min) - timestamps.append(stats.max) + min_max = {} + for idx, name in enumerate(meta.schema.names): + col = [] + for i in range(meta.num_row_groups): + stats = meta.row_group(i).column(idx).statistics + col.append(stats.min) + col.append(stats.max) + min_max[name] = [min(col), max(col)] - return min(timestamps), max(timestamps) + return min_max -def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[str, dict]: +def get_parquet_metadata(file_paths: list[Path]) -> dict[str, dict]: """ Extracts and organizes metadata from a list of Parquet files. - For each file, the function reads the metadata, adds the filename, and attempts to - extract the minimum and maximum timestamps. "row_groups" entry is removed from FileMetaData. + For each file, the function reads the metadata, adds the filename, + and extracts the minimum and maximum timestamps. + "row_groups" entry is removed from FileMetaData. Args: file_paths (list[Path]): A list of paths to the Parquet files. - time_stamp_col (str): The name of the column containing the timestamps. Returns: dict[str, dict]: A dictionary file index as key and the values as metadata of each file. @@ -250,12 +251,8 @@ def get_parquet_metadata(file_paths: list[Path], time_stamp_col: str) -> dict[st # Add the filename to the metadata dictionary metadata_dict["filename"] = str(file_path.name) - # Get the timestamp min and max - try: - start, end = get_timestamp_stats(file_meta, time_stamp_col) - metadata_dict["time_stamps"] = np.array([start, end]) - except ValueError: - pass + # Get column min and max values + metadata_dict["columns"] = get_stats(file_meta) # Remove "row_groups" as they contain a lot of info that is not needed metadata_dict.pop("row_groups", None) From 79440438e9caddc559f1039be736760690f4e1ae Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 17 Jul 2024 18:32:02 +0200 Subject: [PATCH 165/300] roll back the buffer_handler with small changes --- sed/loader/flash/buffer_handler.py | 145 ++++++++++++++--------------- sed/loader/flash/dataframe.py | 10 +- sed/loader/flash/utils.py | 10 +- 3 files changed, 76 insertions(+), 89 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index dac1df41..10c17ca9 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -21,14 +21,15 @@ class BufferFilePaths: """ - A class for handling the paths to the raw and buffer files. + A class for handling the paths to the raw and buffer files of electron and timed dataframes. A list of file sets (dict) are created for each H5 file containing the paths to the raw file and the electron and timed buffer files. Structure of the file sets: { "raw": Path to the H5 file, - "buffer": Path to the buffer file + "electron": Path to the electron buffer file, + "timed": Path to the timed buffer file, } """ @@ -48,7 +49,7 @@ def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: self._file_paths = [ { "raw": h5_path, - "buffer": folder / h5_path.stem, + **{typ: folder / f"{typ}_{h5_path.stem}{suffix}" for typ in DF_TYP}, } for h5_path in h5_paths ] @@ -68,7 +69,7 @@ def file_sets_to_process(self, force_recreate: bool = False) -> list[dict[str, P """Returns a list of file sets that need to be processed.""" if force_recreate: return self._file_paths - return [file_set for file_set in self._file_paths if not file_set["buffer"].exists()] + return [file_set for file_set in self if any(not file_set[key].exists() for key in DF_TYP)] class BufferHandler: @@ -97,17 +98,14 @@ def __init__( ) self.metadata: dict = {} - def _schema_check(self) -> None: + def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: """ Checks the schema of the Parquet files. Raises: ValueError: If the schema of the Parquet files does not match the configuration. """ - expected_schema_set = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), - ) - existing = [file for file in self.fp["buffer"] if file.exists()] + existing = [file for file in files if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing] for filename, schema in zip(existing, parquet_schemas): @@ -128,20 +126,38 @@ def _schema_check(self) -> None: "Please check the configuration file or set force_recreate to True.", ) - def _save_buffer_file(self, path: dict[str, Path]) -> None: + def _save_buffer_file(self, paths: dict[str, Path]) -> None: """ Creates the electron and timed buffer files from the raw H5 file. - + First the dataframe is accessed and forward filled in the non-electron channels. + Then the data types are set. For the electron dataframe, all values not in the electron + channels are dropped. For the timed dataframe, only the train and pulse channels are taken + and it pulse resolved (no longer electron resolved). Both are saved as parquet files. Args: paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. """ # Create a DataFrameCreator instance and the h5 file - DataFrameCreator(config_dataframe=self._config, h5_path=path["raw"]).df.to_parquet( - path["buffer"], + df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df + + # forward fill all the non-electron channels + df[self.fill_channels] = df[self.fill_channels].ffill() + + # Reset the index of the DataFrame and save both the electron and timed dataframes + # electron resolved dataframe + electron_channels = get_channels(self._config["channels"], "per_electron") + dtypes = get_dtypes(self._config["channels"], df.columns.values) + df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( + paths["electron"], ) + # timed dataframe + # drop the electron channels and only take rows with the first electronId + df_timed = df[self.fill_channels].loc[:, :, 0] + dtypes = get_dtypes(self._config["channels"], df_timed.columns.values) + df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) + def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ Creates the buffer files that are missing. @@ -162,28 +178,6 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: delayed(self._save_buffer_file)(file_set) for file_set in file_sets ) - def _fill_dataframes(self, df): - """ - Reads all parquet files into one dataframe using dask and fills NaN values lazily. - """ - # Forward fill the non-electron channels across files - overlap = min(file["num_rows"] for file in self.metadata["file_statistics"].values()) - iterations = self._config.get("forward_fill_iterations", 2) - df = forward_fill_lazy( - df=df, - columns=self.fill_channels, - before=overlap, - iterations=iterations, - ) - # TODO: This dict should be returned by forward_fill_lazy - filling = { - "columns": self.fill_channels, - "overlap": overlap, - "iterations": iterations, - } - self.metadata.update({"filling": filling}) - return df - def _get_dataframes(self) -> None: """ Reads the buffer files from a folder. @@ -196,19 +190,32 @@ def _get_dataframes(self) -> None: it pulse resolved (no longer electron resolved). If time_index is True, the timeIndex is calculated and set as the index (slow operation). """ - df = dd.read_parquet(self.fp["buffer"], calculate_divisions=True) - # Get the metadata from the parquet files - file_stats = get_parquet_metadata( - self.fp["buffer"], - ) - self.metadata.update({"file_statistics": file_stats}) - df = self._fill_dataframes(df) - - # Electron dataframe - electron_dtypes = get_dtypes(self._config["channels"], "per_electron") - self.df["electron"] = df.dropna( - subset=get_channels(self._config["channels"], "per_electron"), - ).astype(electron_dtypes) + # Loop over the electron and timed dataframes + file_stats = {} + filling = {} + for typ in DF_TYP: + # Read the parquet files into a dask dataframe + df = dd.read_parquet(self.fp[typ], calculate_divisions=True) + # Get the metadata from the parquet files + file_stats[typ] = get_parquet_metadata(self.fp[typ]) + + # Forward fill the non-electron channels across files + overlap = min(file["num_rows"] for file in file_stats[typ].values()) + df = forward_fill_lazy( + df=df, + columns=self.fill_channels, + before=overlap, + iterations=1, + ) + # TODO: This dict should be returned by forward_fill_lazy + filling[typ] = { + "columns": self.fill_channels, + "overlap": overlap, + "iterations": 1, + } + + self.df[typ] = df + self.metadata.update({"file_statistics": file_stats, "filling": filling}) # Correct the 3-bit shift which encodes the detector ID in the 8s time if self._config.get("split_sector_id_from_dld_time", False): self.df["electron"], meta = split_dld_time_from_sector_id( @@ -217,31 +224,6 @@ def _get_dataframes(self) -> None: ) self.metadata.update(meta) - # Timed dataframe - df_timed = df[df["electronId"] == 0] - timed_channels = get_channels( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) + ["pulseId"] - df_timed = df_timed[timed_channels].dropna() - timed_dtypes = get_dtypes( - self._config["channels"], - ["per_pulse", "per_train"], - extend_aux=True, - ) - self.df["timed"] = df_timed.astype(timed_dtypes) - - if self._config.get("time_index", False): - # Calculate the time delta for each pulseId (1 microsecond per pulse) - df_timed["timeIndex"] = dd.to_datetime( - df_timed["timeStamp"], - unit="s", - ) + dd.to_timedelta(df_timed["pulseId"], unit="us") - - # Set the new fine timeStamp as the index if needed - self.df["timed"] = df_timed.set_index("timeIndex") - def run( self, h5_paths: list[Path], @@ -252,6 +234,8 @@ def run( ) -> None: """ Runs the buffer file creation process. + Does a schema check on the buffer files and creates them if they are missing. + Performs forward filling and splits the sector ID from the DLD time lazily. Args: h5_paths (List[Path]): List of paths to H5 files. @@ -262,7 +246,20 @@ def run( """ self.fp = BufferFilePaths(h5_paths, folder, suffix) - self._schema_check() + if not force_recreate: + schema_set = set( + get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + ) + self._schema_check(self.fp["electron"], schema_set) + schema_set = set( + get_channels( + self._config["channels"], + formats=["per_pulse", "per_train"], + index=True, + extend_aux=True, + ), + ) - {"electronId"} + self._schema_check(self.fp["timed"], schema_set) self._save_buffer_files(force_recreate, debug) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index a46e03ed..687abe84 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -302,12 +302,4 @@ def df(self) -> pd.DataFrame: self.validate_channel_keys() # been tested with merge, join and concat # concat offers best performance, almost 3 times faster - dataframe = ( - pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1) - .sort_index() - .reset_index() - ) - # we can set dtype to uint16 for index pulseId and electronId - dataframe["pulseId"] = dataframe["pulseId"].astype("uint16") - dataframe["electronId"] = dataframe["electronId"].astype("uint16") - return dataframe.set_index("trainId") + return pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 1ff9a274..7b958921 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -83,21 +83,19 @@ def get_channels( return channels -def get_dtypes(channels_dict: dict, formats: str | list[str], extend_aux: bool = False) -> dict: +def get_dtypes(channels_dict: dict, df_cols: list) -> dict: """Returns a dictionary of channels and their corresponding data types. Currently Auxiliary channels are not included in the dtype dictionary. Args: - channels_dict (dict): The dictionary containing the channels. - formats (str | list[str]): The desired format(s). - extend_aux (bool): If True, includes auxiliary channels. + channels_dict (dict): The config dictionary containing the channels. + df_cols (list): A list of channels in the DataFrame. Returns: dict: A dictionary of channels and their corresponding data types. """ - channels = get_channels(channel_dict=channels_dict, formats=formats, extend_aux=extend_aux) dtypes = {} - for channel in channels: + for channel in df_cols: try: dtypes[channel] = channels_dict[channel].get("dtype") except KeyError: From adfd335f9af85713e9e462c746ae463b858bf5c1 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 17 Jul 2024 18:57:34 +0200 Subject: [PATCH 166/300] fix some test issues --- tests/data/loader/flash/config.yaml | 2 +- tests/loader/flash/test_buffer_handler.py | 10 +++++----- tests/loader/flash/test_dataframe_creator.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index ab4a310c..1e27d89a 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -115,7 +115,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - dldAuxChannels: + aux_channels: sampleBias: slice: 0 dtype: float64 diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 6a83e398..82911cf9 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -33,10 +33,10 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: which HDF5 files need to be read and properly manages the paths for saving buffer files. It follows these steps: 1. Creates a directory structure for storing buffer files and initializes the BufferHandler. - 2. Checks if the to_process method populates the dict of missing file sets and + 2. Checks if the file_sets_to_process method populates the dict of missing file sets and verify that initially, all provided files are considered missing. 3. Checks that the paths for saving buffer files are correctly generated. - 4. Creates a single buffer file and reruns to_process to ensure that the BufferHandler + 4. Creates a single buffer file and reruns file_sets_to_process to ensure that the BufferHandler recognizes one less missing file. 5. Checks if the force_recreate parameter forces the BufferHandler to consider all files 6. Cleans up by removing the created buffer file. @@ -47,7 +47,7 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: fp = BufferFilePaths(h5_paths, folder, "") # check that all files are to be read - assert len(fp.to_process()) == len(h5_paths) + assert len(fp.file_sets_to_process()) == len(h5_paths) print(folder) # create expected paths expected_buffer_electron_paths = [ @@ -71,10 +71,10 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: # check again for files to read and expect one less file fp = BufferFilePaths(h5_paths, folder, "") # check that only one file is to be read - assert len(fp.to_process()) == len(h5_paths) - 1 + assert len(fp.file_sets_to_process()) == len(h5_paths) - 1 # check that both files are to be read if force_recreate is set to True - assert len(fp.to_process(force_recreate=True)) == len(h5_paths) + assert len(fp.file_sets_to_process(force_recreate=True)) == len(h5_paths) # remove buffer files Path(path["electron"]).unlink() diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index a84b417f..dc50efea 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -234,7 +234,7 @@ def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path] # The subchannels are stored in the second dimension # Only index amount of values are stored in the first dimension, the rest are NaNs # hence the slicing - subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] + subchannels = config_dataframe["channels"]["dldAux"]["aux_channels"] for subchannel, index in subchannels.items(): assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, index]) From fbd5cbf97a64d3dc84f346d54c00799c7acdf26b Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 18 Jul 2024 09:58:44 +0200 Subject: [PATCH 167/300] fix energy calibration and delay range --- tutorial/4_hextof_workflow.ipynb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 4de9c825..5a76a896 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -497,7 +497,6 @@ "outputs": [], "source": [ "sp.calibrate_energy_axis(\n", - " ref_id=4,\n", " ref_energy=-.55,\n", " method=\"lmfit\",\n", " energy_scale='kinetic',\n", @@ -792,7 +791,7 @@ "axes = ['energy','delayStage']\n", "bins = [100,150]\n", "delay_start,delay_stop=1462.00,1464.85\n", - "ranges = [[-3,2], [-1.15, 1.7]]\n", + "ranges = [[-3,2], [-1.1, 1.75]]\n", "res = sp.compute(bins=bins, axes=axes, ranges=ranges)" ] }, @@ -914,7 +913,7 @@ "axes = ['energy','delayStage']\n", "bins = [100,150]\n", "delay_start,delay_stop=1462.00,1464.85\n", - "ranges = [[-5,2], [-1.15, 1.7]]\n", + "ranges = [[-5,2], [-1.1, 1.75]]\n", "res = sp.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" ] }, @@ -955,6 +954,13 @@ "source": [ "sp.save('binned.tiff')" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From 4d35e542d011b933448856b33c96bb5310d2441f Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 18 Jul 2024 14:08:52 +0200 Subject: [PATCH 168/300] some metadata changes --- sed/config/flash_example_config.yaml | 10 +++++++-- sed/loader/flash/buffer_handler.py | 8 +++---- sed/loader/flash/dataframe.py | 13 ++++++----- sed/loader/flash/loader.py | 9 ++++---- sed/loader/flash/utils.py | 23 +++++++++++--------- sed/loader/utils.py | 13 ++++++----- tests/data/loader/flash/config.yaml | 2 +- tests/loader/flash/test_buffer_handler.py | 2 +- tests/loader/flash/test_dataframe_creator.py | 14 ++++++------ tests/loader/flash/test_flash_loader.py | 19 ++++++++-------- tests/loader/flash/test_utils.py | 3 ++- 11 files changed, 65 insertions(+), 51 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index a09dd9f2..dbc2ee73 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -66,6 +66,10 @@ dataframe: corrected_tof_column: "tm" # the time stamp column time_stamp_alias: timeStamp + # auxiliary channel alias + aux_alias: dldAux + # aux subchannels alias + aux_subchannels_alias: dldAuxChannels # time length of a base time-of-flight bin in seconds tof_binwidth: 2.0576131995767355E-11 # binning parameter for time-of-flight data. @@ -102,8 +106,10 @@ dataframe: # channels have the following structure: # channelAlias: # format: per_pulse/per_electron/per_train - # group_name: the hdf5 group path + # index_key: the hdf5 index key + # dataset_key: the hdf5 dataset key # slice: if the group contains multidimensional data, where to slice + # dtype: the datatype of the data channels: # The timestamp @@ -153,7 +159,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - aux_channels: + dldAuxChannels: sampleBias: slice: 0 dtype: float32 diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 10c17ca9..6217f3a2 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -92,7 +92,7 @@ def __init__( self.fp: BufferFilePaths = None self.df: dict[str, dd.DataFrame] = {typ: None for typ in DF_TYP} self.fill_channels: list[str] = get_channels( - self._config["channels"], + self._config, ["per_pulse", "per_train"], extend_aux=True, ) @@ -146,7 +146,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: # Reset the index of the DataFrame and save both the electron and timed dataframes # electron resolved dataframe - electron_channels = get_channels(self._config["channels"], "per_electron") + electron_channels = get_channels(self._config, "per_electron") dtypes = get_dtypes(self._config["channels"], df.columns.values) df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( paths["electron"], @@ -248,12 +248,12 @@ def run( if not force_recreate: schema_set = set( - get_channels(self._config["channels"], formats="all", index=True, extend_aux=True), + get_channels(self._config, formats="all", index=True, extend_aux=True), ) self._schema_check(self.fp["electron"], schema_set) schema_set = set( get_channels( - self._config["channels"], + self._config, formats=["per_pulse", "per_train"], index=True, extend_aux=True, diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 687abe84..a3780a56 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -153,7 +153,7 @@ def df_electron(self) -> pd.DataFrame: index, indexer = self.pulse_index(offset) # Get the relevant channels and their slice index - channels = get_channels(self._config["channels"], "per_electron") + channels = get_channels(self._config, "per_electron") slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] # First checking if dataset keys are the same for all channels @@ -204,9 +204,9 @@ def df_pulse(self) -> pd.DataFrame: """ series = [] # Get the relevant channel names - channels = get_channels(self._config["channels"], "per_pulse") + channels = get_channels(self._config, "per_pulse") # check if dldAux is in the channels and raise error if so - if "dldAux" in channels: + if self._config.get("aux_alias", "dldAux") in channels: raise ValueError( "dldAux is a 'per_train' channel. " "Please choose 'per_train' as the format for dldAux.", @@ -244,7 +244,7 @@ def df_train(self) -> pd.DataFrame: """ series = [] # Get the relevant channel names - channels = get_channels(self._config["channels"], "per_train") + channels = get_channels(self._config, "per_train") # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned @@ -259,15 +259,16 @@ def df_train(self) -> pd.DataFrame: # contains multiple channels inside. Even though they are resolved per train, # they come in pulse format, so the extra values are sliced and individual channels are # created and appended to the list + aux_alias = self._config.get("aux_alias", "dldAux") + sub_channels = self._config.get("aux_subchannels_alias", "dldAuxChannels") if channel == "dldAux": - aux_channels = self._config["channels"]["dldAux"]["aux_channels"].items() + aux_channels = self._config["channels"][aux_alias][sub_channels].items() for name, values in aux_channels: series.append( pd.Series( dataset[: key.size, values["slice"]], index, name=name, - dtype=values["dtype"], ), ) else: diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 61eab4f5..23fe655a 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -215,20 +215,21 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[f KeyError: If a file ID in fids or a run ID in 'runs' does not exist in the metadata. """ try: - file_statistics = self.metadata["file_statistics"] + file_statistics = self.metadata["file_statistics"]["timed"] except Exception as exc: raise KeyError( "File statistics missing. Use 'read_dataframe' first.", ) from exc + time_stamp_alias = self._config["dataframe"].get("time_stamp_alias", "timeStamp") def get_elapsed_time_from_fid(fid): try: fid = str(fid) # Ensure the key is a string - time_stamps = file_statistics[fid]["columns"]["timeStamp"] - elapsed_time = max(time_stamps) - min(time_stamps) + time_stamps = file_statistics[fid]["columns"][time_stamp_alias] + elapsed_time = time_stamps["max"] - time_stamps["min"] except KeyError as exc: raise KeyError( - f"Timestamp metadata missing in file {fid}." + f"Timestamp metadata missing in file {fid}. " "Add timestamp column and alias to config before loading.", ) from exc diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 7b958921..c77256dc 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -4,13 +4,11 @@ # TODO: move to config MULTI_INDEX = ["trainId", "pulseId", "electronId"] PULSE_ALIAS = MULTI_INDEX[1] -DLD_AUX_ALIAS = "dldAux" -AUX_CHANNELS = "aux_channels" FORMATS = ["per_electron", "per_pulse", "per_train"] def get_channels( - channel_dict: dict = None, + config_dataframe: dict = {}, formats: str | list[str] = None, index: bool = False, extend_aux: bool = False, @@ -20,15 +18,20 @@ def get_channels( 'all' returns all channels but 'pulseId' and 'dldAux' (if not extended). Args: + config_dataframe (dict): The config dictionary containing the dataframe keys. formats (str | list[str]): The desired format(s) ('per_pulse', 'per_electron', 'per_train', 'all'). index (bool): If True, includes channels from the multiindex. - extend_aux (bool): If True, includes channels from the 'dldAuxChannels' dictionary, - else includes 'dldAux'. + extend_aux (bool): If True, includes channels from the subchannels of the auxiliary channel. + else just includes the auxiliary channel alias. Returns: List[str]: A list of channels with the specified format(s). """ + channel_dict = config_dataframe.get("channels", {}) + dld_aux_alias = config_dataframe.get("aux_alias", "dldAux") + aux_subchannels_alias = config_dataframe.get("aux_subchannels_alias", "dldAuxChannels") + # If 'formats' is a single string, convert it to a list for uniform processing. if isinstance(formats, str): formats = [formats] @@ -36,7 +39,7 @@ def get_channels( # If 'formats' is a string "all", gather all possible formats. if formats == ["all"]: channels = get_channels( - channel_dict, + config_dataframe, FORMATS, index, extend_aux, @@ -68,17 +71,17 @@ def get_channels( channels.extend( key for key in available_channels - if channel_dict[key]["format"] == format_ and key != DLD_AUX_ALIAS + if channel_dict[key]["format"] == format_ and key != dld_aux_alias ) # Include 'dldAuxChannels' if the format is 'per_train' and extend_aux is True. # Otherwise, include 'dldAux'. - if format_ == FORMATS[2] and DLD_AUX_ALIAS in available_channels: + if format_ == FORMATS[2] and dld_aux_alias in available_channels: if extend_aux: channels.extend( - channel_dict[DLD_AUX_ALIAS][AUX_CHANNELS].keys(), + channel_dict[dld_aux_alias][aux_subchannels_alias].keys(), ) else: - channels.extend([DLD_AUX_ALIAS]) + channels.extend([dld_aux_alias]) return channels diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 5d7c015e..6bcce9f8 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -206,7 +206,7 @@ def split_dld_time_from_sector_id( return df, {"split_dld_time_from_sector_id": metadata} -def get_stats(meta: pq.FileMetaData) -> dict[str, list[int]]: +def get_stats(meta: pq.FileMetaData) -> dict: """ Extracts the minimum and maximum of all columns from the metadata of a Parquet file. @@ -221,10 +221,13 @@ def get_stats(meta: pq.FileMetaData) -> dict[str, list[int]]: col = [] for i in range(meta.num_row_groups): stats = meta.row_group(i).column(idx).statistics - col.append(stats.min) - col.append(stats.max) - min_max[name] = [min(col), max(col)] - + if stats is not None: + if stats.min is not None: + col.append(stats.min) + if stats.max is not None: + col.append(stats.max) + if col: + min_max[name] = {"min": min(col), "max": max(col)} return min_max diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 1e27d89a..ab4a310c 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -115,7 +115,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - aux_channels: + dldAuxChannels: sampleBias: slice: 0 dtype: float64 diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index 82911cf9..f2c99901 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -238,7 +238,7 @@ def test_get_filled_dataframe(config: dict, h5_paths: list[Path]) -> None: channel_pulse = set( get_channels( - config["dataframe"]["channels"], + config["dataframe"], formats=["per_pulse", "per_train"], index=True, extend_aux=True, diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index dc50efea..baa23f33 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -154,7 +154,7 @@ def test_df_electron(config_dataframe: dict, h5_paths: list[Path]) -> None: # check that dataframe contains all subchannels assert np.all( - set(result_df.columns) == set(get_channels(config_dataframe["channels"], ["per_electron"])), + set(result_df.columns) == set(get_channels(config_dataframe, ["per_electron"])), ) @@ -184,7 +184,7 @@ def test_create_dataframe_per_pulse(config_dataframe: dict, h5_paths: list[Path] # assert that dataframe contains all channels assert np.all( - set(result_df.columns) == set(get_channels(config_dataframe["channels"], ["per_pulse"])), + set(result_df.columns) == set(get_channels(config_dataframe, ["per_pulse"])), ) @@ -205,13 +205,13 @@ def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path] # check that dataframe contains all channels assert np.all( set(result_df.columns) - == set(get_channels(config_dataframe["channels"], ["per_train"], extend_aux=True)), + == set(get_channels(config_dataframe, ["per_train"], extend_aux=True)), ) # Ensure DataFrame has rows equal to unique keys from "per_train" channels, considering # different channels may have data for different trains. This checks the DataFrame's # completeness and integrity, especially important when channels record at varying trains. - channels = get_channels(config_dataframe["channels"], ["per_train"]) + channels = get_channels(config_dataframe, ["per_train"]) all_keys = Index([]) for channel in channels: # Append unique keys from each channel, considering only training data @@ -234,9 +234,9 @@ def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path] # The subchannels are stored in the second dimension # Only index amount of values are stored in the first dimension, the rest are NaNs # hence the slicing - subchannels = config_dataframe["channels"]["dldAux"]["aux_channels"] - for subchannel, index in subchannels.items(): - assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, index]) + subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] + for subchannel, values in subchannels.items(): + assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, values["slice"]]) assert result_df.index.is_unique diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index be28e659..a34a9977 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -138,9 +138,9 @@ def test_get_elapsed_time_fid(config: dict) -> None: fl.metadata = { "file_statistics": { "timed": { - "0": {"time_stamps": [10, 20]}, - "1": {"time_stamps": [20, 30]}, - "2": {"time_stamps": [30, 40]}, + "0": {"columns": {"timeStamp": {"min": 10, "max": 20}}}, + "1": {"columns": {"timeStamp": {"min": 20, "max": 30}}}, + "2": {"columns": {"timeStamp": {"min": 30, "max": 40}}}, }, }, } @@ -167,7 +167,7 @@ def test_get_elapsed_time_fid(config: dict) -> None: "file_statistics": { "timed": { "0": {}, - "1": {"time_stamps": [20, 30]}, + "1": {"columns": {"timeStamp": {"min": 20, "max": 30}}}, }, }, } @@ -186,15 +186,15 @@ def test_get_elapsed_time_run(config: dict) -> None: } config_ = config.copy() data_parquet_dir = create_parquet_dir(config_, "get_elapsed_time_run") - config_["core"]["paths"]["data_parquet_dir"] = data_parquet_dir + config_["core"]["paths"]["processed"] = data_parquet_dir # Create an instance of FlashLoader fl = FlashLoader(config=config_) fl.read_dataframe(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"]["electron"]["0"]["time_stamps"] - expected_elapsed_time_0 = end - start - start, end = fl.metadata["file_statistics"]["electron"]["1"]["time_stamps"] - expected_elapsed_time_1 = end - start + min_max = fl.metadata["file_statistics"]["electron"]["0"]["columns"]["timeStamp"] + expected_elapsed_time_0 = min_max["max"] - min_max["min"] + min_max = fl.metadata["file_statistics"]["electron"]["1"]["columns"]["timeStamp"] + expected_elapsed_time_1 = min_max["max"] - min_max["min"] elapsed_time = fl.get_elapsed_time(runs=[43878]) assert elapsed_time == expected_elapsed_time_0 @@ -203,7 +203,6 @@ def test_get_elapsed_time_run(config: dict) -> None: assert elapsed_time == [expected_elapsed_time_0, expected_elapsed_time_1] elapsed_time = fl.get_elapsed_time(runs=[43878, 43879]) - start, end = fl.metadata["file_statistics"]["electron"]["1"]["time_stamps"] assert elapsed_time == expected_elapsed_time_0 + expected_elapsed_time_1 # remove the parquet files diff --git a/tests/loader/flash/test_utils.py b/tests/loader/flash/test_utils.py index 82ec048d..929a9305 100644 --- a/tests/loader/flash/test_utils.py +++ b/tests/loader/flash/test_utils.py @@ -25,11 +25,12 @@ def test_get_channels_by_format(config_dataframe: dict) -> None: retrieving channels based on formats and index inclusion. """ # Initialize the FlashLoader instance with the given config_file. - ch_dict = config_dataframe["channels"] + ch_dict = config_dataframe # Call get_channels method with different format options. # Request channels for 'per_electron' format using a list. + print(ch_dict["channels"]) format_electron = get_channels(ch_dict, ["per_electron"]) # Request channels for 'per_pulse' format using a string. From 0aab5c49876d412809cdce48490838ccd6a99e8e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 18 Jul 2024 14:18:55 +0200 Subject: [PATCH 169/300] use correct lock --- poetry.lock | 138 ++++++++++++++++++++++------------------------------ 1 file changed, 57 insertions(+), 81 deletions(-) diff --git a/poetry.lock b/poetry.lock index 958a7b22..fbb2b675 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -912,17 +912,6 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] -[[package]] -name = "entrypoints" -version = "0.4" -description = "Discover and load entry points from installed packages." -optional = false -python-versions = ">=3.6" -files = [ - {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, - {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -1592,27 +1581,26 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.4.9" +version = "8.6.2" description = "Jupyter protocol implementation and client libraries" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"}, - {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"}, + {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, + {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, ] [package.dependencies] -entrypoints = "*" -jupyter-core = ">=4.9.2" -nest-asyncio = ">=1.5.4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" tornado = ">=6.2" -traitlets = "*" +traitlets = ">=5.3" [package.extras] -doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] [[package]] name = "jupyter-console" @@ -2319,40 +2307,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.1" +version = "3.9.0" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, - {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, - {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, - {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, - {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, - {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, - {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, - {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, - {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, - {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, - {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, + {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, + {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, + {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, + {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, + {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, + {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, + {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, + {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, + {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, ] [package.dependencies] @@ -2699,13 +2687,13 @@ files = [ [[package]] name = "notebook" -version = "6.5.7" +version = "6.5.4" description = "A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.7" files = [ - {file = "notebook-6.5.7-py3-none-any.whl", hash = "sha256:a6afa9a4ff4d149a0771ff8b8c881a7a73b3835f9add0606696d6e9d98ac1cd0"}, - {file = "notebook-6.5.7.tar.gz", hash = "sha256:04eb9011dfac634fbd4442adaf0a8c27cd26beef831fe1d19faf930c327768e4"}, + {file = "notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f"}, + {file = "notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e"}, ] [package.dependencies] @@ -2713,7 +2701,7 @@ argon2-cffi = "*" ipykernel = "*" ipython-genutils = "*" jinja2 = "*" -jupyter-client = ">=5.3.4,<8" +jupyter-client = ">=5.3.4" jupyter-core = ">=4.6.1" nbclassic = ">=0.4.7" nbconvert = ">=5" @@ -2845,12 +2833,14 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] [[package]] @@ -2932,6 +2922,7 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -2945,12 +2936,14 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -2961,9 +2954,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3683,6 +3676,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -4212,24 +4206,6 @@ objc = ["pyobjc-framework-Cocoa"] win32 = ["pywin32"] [[package]] -<<<<<<< HEAD -name = "setuptools" -version = "70.3.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, - {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -======= ->>>>>>> fix-459 name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" From 1966ac43305e46006b3a5ddaf48fd400195eff10 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 18 Jul 2024 14:29:51 +0200 Subject: [PATCH 170/300] roll back to iterations --- sed/loader/flash/buffer_handler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 6217f3a2..0d564d2a 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -201,17 +201,18 @@ def _get_dataframes(self) -> None: # Forward fill the non-electron channels across files overlap = min(file["num_rows"] for file in file_stats[typ].values()) + iterations = self._config.get("forward_fill_iterations", 2) df = forward_fill_lazy( df=df, columns=self.fill_channels, before=overlap, - iterations=1, + iterations=iterations, ) # TODO: This dict should be returned by forward_fill_lazy filling[typ] = { "columns": self.fill_channels, "overlap": overlap, - "iterations": 1, + "iterations": iterations, } self.df[typ] = df From d09e715468ac22b6cefe509549ad9d5da4077d37 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 23 Jul 2024 18:09:06 +0200 Subject: [PATCH 171/300] add aux alias and subchannels argument --- sed/config/flash_example_config.yaml | 16 +++---- sed/loader/flash/buffer_handler.py | 5 +- sed/loader/flash/dataframe.py | 49 ++++++++++---------- sed/loader/flash/utils.py | 19 ++++---- tests/data/loader/flash/config.yaml | 2 +- tests/loader/flash/test_dataframe_creator.py | 8 ++-- 6 files changed, 51 insertions(+), 48 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index baf3ef42..ccc7dcac 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -104,12 +104,17 @@ dataframe: # The channels to load. # channels have the following structure: - # channelAlias: + # : # format: per_pulse/per_electron/per_train # index_key: the hdf5 index key # dataset_key: the hdf5 dataset key - # slice: if the group contains multidimensional data, where to slice + # slice: int to slice a multidimensional data along axis=1. If not defined, there is no slicing # dtype: the datatype of the data + # subChannels: further aliases for if the data is multidimensional and needs to be split in different cols + # used currently for the auxiliary channel + # : + # slice: int to slice a multidimensional data along axis=1. Must be defined + # dtype: the datatype of the data channels: # The timestamp @@ -159,7 +164,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - dldAuxChannels: + subChannels: sampleBias: slice: 0 dtype: float32 @@ -168,19 +173,14 @@ dataframe: dtype: float64 extractorVoltage: slice: 2 - dtype: float64 extractorCurrent: slice: 3 - dtype: float64 cryoTemperature: slice: 4 - dtype: float64 sampleTemperature: slice: 5 - dtype: float64 dldTimeBinSize: slice: 15 - dtype: float64 # ADC containing the pulser sign (1: value approx. 35000, 0: 33000) pulserSignAdc: diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 0d564d2a..b1b48e38 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -147,7 +147,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: # Reset the index of the DataFrame and save both the electron and timed dataframes # electron resolved dataframe electron_channels = get_channels(self._config, "per_electron") - dtypes = get_dtypes(self._config["channels"], df.columns.values) + dtypes = get_dtypes(self._config, df.columns.values) df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( paths["electron"], ) @@ -155,7 +155,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: # timed dataframe # drop the electron channels and only take rows with the first electronId df_timed = df[self.fill_channels].loc[:, :, 0] - dtypes = get_dtypes(self._config["channels"], df_timed.columns.values) + dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: @@ -173,6 +173,7 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: if debug: for file_set in file_sets: self._save_buffer_file(file_set) + print(f"Processed {file_set['raw'].stem}") else: Parallel(n_jobs=n_cores, verbose=10)( delayed(self._save_buffer_file)(file_set) for file_set in file_sets diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index a3780a56..d9a3ace0 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -62,18 +62,18 @@ def get_index_dataset_key(self, channel: str) -> tuple[str, str]: def get_dataset_array( self, channel: str, - slice_: bool = False, - ) -> tuple[pd.Index, h5py.Dataset]: + slice_: bool = True, + ) -> tuple[pd.Index, np.ndarray | h5py.Dataset]: """ Returns a numpy array for a given channel name. Args: channel (str): The name of the channel. - slice_ (bool): If True, applies slicing on the dataset. + slice_ (bool): Applies slicing on the dataset. Default is True. Returns: - tuple[pd.Index, h5py.Dataset]: A tuple containing the train ID - pd.Index and the numpy array for the channel's data. + tuple[pd.Index, np.ndarray | h5py.Dataset]: A tuple containing the train ID + pd.Index and the channel's data. """ # Get the data from the necessary h5 file and channel index_key, dataset_key = self.get_index_dataset_key(channel) @@ -85,9 +85,9 @@ def get_dataset_array( slice_index = self._config["channels"][channel].get("slice", None) if slice_index is not None: dataset = np.take(dataset, slice_index, axis=1) - # If np_array is size zero, fill with NaNs + # If np_array is size zero, fill with NaNs, fill it with NaN values + # of the same shape as index if dataset.shape[0] == 0: - # Fill the np_array with NaN values of the same shape as train_id dataset = np.full_like(key, np.nan, dtype=np.double) return key, dataset @@ -105,7 +105,7 @@ def pulse_index(self, offset: int) -> tuple[pd.MultiIndex, slice | np.ndarray]: the indexer. """ # Get the pulse_dataset and the train_index - train_index, pulse_dataset = self.get_dataset_array("pulseId", slice_=True) + train_index, pulse_dataset = self.get_dataset_array("pulseId") # pulse_dataset comes as a 2D array, resolved per train. Here it is flattened # the daq has an offset so no pulses are missed. This offset is subtracted here pulse_ravel = pulse_dataset.ravel() - offset @@ -154,6 +154,8 @@ def df_electron(self) -> pd.DataFrame: # Get the relevant channels and their slice index channels = get_channels(self._config, "per_electron") + if channels == []: + return pd.DataFrame() slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] # First checking if dataset keys are the same for all channels @@ -165,16 +167,15 @@ def df_electron(self) -> pd.DataFrame: # If all dataset keys are the same, we only need to load the dataset once and slice # the appropriate columns. This is much faster than loading the same dataset multiple times if all_keys_same: - _, dataset = self.get_dataset_array(channels[0]) + _, dataset = self.get_dataset_array(channels[0], slice_=False) data_dict = { - channel: dataset[:, slice_, :].ravel() - for channel, slice_ in zip(channels, slice_index) + channel: dataset[:, idx, :].ravel() for channel, idx in zip(channels, slice_index) } dataframe = pd.DataFrame(data_dict) # In case channels do differ, we create a pd.Series for each channel and concatenate them else: series = { - channel: pd.Series(self.get_dataset_array(channel, slice_=True)[1].ravel()) + channel: pd.Series(self.get_dataset_array(channel)[1].ravel()) for channel in channels } dataframe = pd.concat(series, axis=1) @@ -205,16 +206,12 @@ def df_pulse(self) -> pd.DataFrame: series = [] # Get the relevant channel names channels = get_channels(self._config, "per_pulse") - # check if dldAux is in the channels and raise error if so - if self._config.get("aux_alias", "dldAux") in channels: - raise ValueError( - "dldAux is a 'per_train' channel. " - "Please choose 'per_train' as the format for dldAux.", - ) + if channels == []: + return pd.DataFrame() # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned - key, dataset = self.get_dataset_array(channel, slice_=True) + key, dataset = self.get_dataset_array(channel) # Electron resolved MultiIndex is created. Since this is pulse data, # the electron index is always 0 index = pd.MultiIndex.from_product( @@ -248,7 +245,7 @@ def df_train(self) -> pd.DataFrame: # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned - key, dataset = self.get_dataset_array(channel, slice_=True) + key, dataset = self.get_dataset_array(channel) # Electron and pulse resolved MultiIndex is created. Since this is train data, # the electron and pulse index is always 0 index = pd.MultiIndex.from_product( @@ -260,10 +257,14 @@ def df_train(self) -> pd.DataFrame: # they come in pulse format, so the extra values are sliced and individual channels are # created and appended to the list aux_alias = self._config.get("aux_alias", "dldAux") - sub_channels = self._config.get("aux_subchannels_alias", "dldAuxChannels") - if channel == "dldAux": - aux_channels = self._config["channels"][aux_alias][sub_channels].items() - for name, values in aux_channels: + if channel == aux_alias: + try: + sub_channels = self._config["channels"][aux_alias]["subChannels"] + except KeyError: + raise KeyError( + f"Provide 'subChannels' for auxiliary channel '{aux_alias}'.", + ) + for name, values in sub_channels.items(): series.append( pd.Series( dataset[: key.size, values["slice"]], diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index c77256dc..3d7843af 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -29,8 +29,7 @@ def get_channels( List[str]: A list of channels with the specified format(s). """ channel_dict = config_dataframe.get("channels", {}) - dld_aux_alias = config_dataframe.get("aux_alias", "dldAux") - aux_subchannels_alias = config_dataframe.get("aux_subchannels_alias", "dldAuxChannels") + aux_alias = config_dataframe.get("aux_alias", "dldAux") # If 'formats' is a single string, convert it to a list for uniform processing. if isinstance(formats, str): @@ -71,39 +70,41 @@ def get_channels( channels.extend( key for key in available_channels - if channel_dict[key]["format"] == format_ and key != dld_aux_alias + if channel_dict[key]["format"] == format_ and key != aux_alias ) # Include 'dldAuxChannels' if the format is 'per_train' and extend_aux is True. # Otherwise, include 'dldAux'. - if format_ == FORMATS[2] and dld_aux_alias in available_channels: + if format_ == FORMATS[2] and aux_alias in available_channels: if extend_aux: channels.extend( - channel_dict[dld_aux_alias][aux_subchannels_alias].keys(), + channel_dict[aux_alias]["subChannels"].keys(), ) else: - channels.extend([dld_aux_alias]) + channels.extend([aux_alias]) return channels -def get_dtypes(channels_dict: dict, df_cols: list) -> dict: +def get_dtypes(config_dataframe: dict, df_cols: list) -> dict: """Returns a dictionary of channels and their corresponding data types. Currently Auxiliary channels are not included in the dtype dictionary. Args: - channels_dict (dict): The config dictionary containing the channels. + config_dataframe (dict): The config dictionary containing the dataframe keys. df_cols (list): A list of channels in the DataFrame. Returns: dict: A dictionary of channels and their corresponding data types. """ + channels_dict = config_dataframe.get("channels", {}) + aux_alias = config_dataframe.get("aux_alias", "dldAux") dtypes = {} for channel in df_cols: try: dtypes[channel] = channels_dict[channel].get("dtype") except KeyError: try: - dtypes[channel] = channels_dict["dldAux"][channel].get("dtype") + dtypes[channel] = channels_dict[aux_alias][channel].get("dtype") except KeyError: dtypes[channel] = None return dtypes diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index ab4a310c..19e01a2a 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -115,7 +115,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - dldAuxChannels: + subChannels: sampleBias: slice: 0 dtype: float64 diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index baa23f33..64e7712c 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -33,7 +33,7 @@ def test_get_dataset_array(config_dataframe: dict, h5_paths: list[Path]) -> None df = DataFrameCreator(config_dataframe, h5_paths[0]) channel = "dldPosX" - train_id, dset = df.get_dataset_array(channel) + train_id, dset = df.get_dataset_array(channel, slice_=False) # Check that the train_id and np_array have the correct shapes and types assert isinstance(train_id, Index) assert isinstance(dset, h5py.Dataset) @@ -61,7 +61,7 @@ def test_empty_get_dataset_array( channel = "gmdTunnel" df = DataFrameCreator(config_dataframe, h5_paths[0]) - train_id, dset = df.get_dataset_array(channel) + train_id, dset = df.get_dataset_array(channel, slice_=False) channel_index_key = "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index" # channel_dataset_key = config_dataframe["channels"][channel]["group_name"] + "value" @@ -77,7 +77,7 @@ def test_empty_get_dataset_array( df = DataFrameCreator(config_dataframe, h5_paths[0]) df.h5_file = h5_file_copy - train_id, dset_empty = df.get_dataset_array(channel) + train_id, dset_empty = df.get_dataset_array(channel, slice_=False) assert dset_empty.shape[0] == train_id.shape[0] assert dset.shape[1] == 8 @@ -234,7 +234,7 @@ def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path] # The subchannels are stored in the second dimension # Only index amount of values are stored in the first dimension, the rest are NaNs # hence the slicing - subchannels = config_dataframe["channels"]["dldAux"]["dldAuxChannels"] + subchannels = config_dataframe["channels"]["dldAux"]["subChannels"] for subchannel, values in subchannels.items(): assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, values["slice"]]) From b0471f2bef22bb30d3e9ae998789e8501b56c628 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 23 Jul 2024 20:25:48 +0200 Subject: [PATCH 172/300] change name and return of the run method --- sed/loader/flash/buffer_handler.py | 9 +++++++-- sed/loader/flash/loader.py | 4 +--- tests/loader/flash/test_buffer_handler.py | 23 ++++++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index b1b48e38..d1a3c24e 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -226,14 +226,14 @@ def _get_dataframes(self) -> None: ) self.metadata.update(meta) - def run( + def process_and_load_dataframe( self, h5_paths: list[Path], folder: Path, force_recreate: bool = False, suffix: str = "", debug: bool = False, - ) -> None: + ) -> tuple[dd.DataFrame, dd.DataFrame]: """ Runs the buffer file creation process. Does a schema check on the buffer files and creates them if they are missing. @@ -245,6 +245,9 @@ def run( force_recreate (bool): Flag to force recreation of buffer files. suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode.): + + Returns: + Tuple[dd.DataFrame, dd.DataFrame]: The electron and timed dataframes. """ self.fp = BufferFilePaths(h5_paths, folder, suffix) @@ -266,3 +269,5 @@ def run( self._save_buffer_files(force_recreate, debug) self._get_dataframes() + + return self.df["electron"], self.df["timed"] diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 8ed600d1..3426f4a1 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -335,15 +335,13 @@ def read_dataframe( # Obtain the parquet filenames, metadata, and schema from the method # which handles buffer file creation/reading h5_paths = [Path(file) for file in self.files] - bh.run( + df, df_timed = bh.process_and_load_dataframe( h5_paths=h5_paths, folder=processed_dir, force_recreate=force_recreate, suffix=detector, debug=debug, ) - df = bh.df["electron"] - df_timed = bh.df["timed"] if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index f2c99901..e879367c 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -109,7 +109,7 @@ def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: """ folder = create_parquet_dir(config, "schema_mismatch") bh = BufferHandler(config) - bh.run(h5_paths=h5_paths, folder=folder, debug=True) + bh.process_and_load_dataframe(h5_paths=h5_paths, folder=folder, debug=True) # Manipulate the configuration to introduce a new channel 'gmdTunnel2' config_dict = config @@ -123,7 +123,7 @@ def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: # Reread the dataframe with the modified configuration, expecting a schema mismatch error with pytest.raises(ValueError) as e: bh = BufferHandler(config) - bh.run(h5_paths=h5_paths, folder=folder, debug=True) + bh.process_and_load_dataframe(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args[0] # Validate the specific error messages for schema mismatch @@ -133,7 +133,7 @@ def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: # Force recreation of the dataframe, including the added channel 'gmdTunnel2' bh = BufferHandler(config) - bh.run(h5_paths=h5_paths, folder=folder, force_recreate=True, debug=True) + bh.process_and_load_dataframe(h5_paths=h5_paths, folder=folder, force_recreate=True, debug=True) # Remove 'gmdTunnel2' from the configuration to simulate a missing channel scenario del config["dataframe"]["channels"]["gmdTunnel2"] @@ -141,7 +141,7 @@ def test_buffer_schema_mismatch(config: dict, h5_paths: list[Path]) -> None: with pytest.raises(ValueError) as e: # Attempt to read the dataframe again to check for the missing channel error bh = BufferHandler(config) - bh.run(h5_paths=h5_paths, folder=folder, debug=True) + bh.process_and_load_dataframe(h5_paths=h5_paths, folder=folder, debug=True) expected_error = e.value.args[0] # Check for the specific error message indicating a missing channel in the configuration @@ -165,11 +165,11 @@ def test_save_buffer_files(config: dict, h5_paths: list[Path]) -> None: """ folder_serial = create_parquet_dir(config, "save_buffer_files_serial") bh_serial = BufferHandler(config) - bh_serial.run(h5_paths, folder_serial, debug=True) + bh_serial.process_and_load_dataframe(h5_paths, folder_serial, debug=True) folder_parallel = create_parquet_dir(config, "save_buffer_files_parallel") bh_parallel = BufferHandler(config) - bh_parallel.run(h5_paths, folder_parallel) + bh_parallel.process_and_load_dataframe(h5_paths, folder_parallel) df_serial = pd.read_parquet(folder_serial) df_parallel = pd.read_parquet(folder_parallel) @@ -201,7 +201,7 @@ def test_save_buffer_files_exception( # testing exception in parallel execution with pytest.raises(ValueError): bh = BufferHandler(config_) - bh.run(h5_paths, folder_parallel, debug=False) + bh.process_and_load_dataframe(h5_paths, folder_parallel, debug=False) # check exception message with empty dataset config_ = deepcopy(config) @@ -223,14 +223,19 @@ def test_save_buffer_files_exception( # expect key error because of missing index dataset with pytest.raises(KeyError): bh = BufferHandler(config_) - bh.run([tmp_path / "copy.h5"], folder_parallel, debug=False, force_recreate=True) + bh.process_and_load_dataframe( + [tmp_path / "copy.h5"], + folder_parallel, + debug=False, + force_recreate=True, + ) def test_get_filled_dataframe(config: dict, h5_paths: list[Path]) -> None: """Test function to verify the creation of a filled dataframe from the buffer files.""" folder = create_parquet_dir(config, "get_filled_dataframe") bh = BufferHandler(config) - bh.run(h5_paths, folder) + bh.process_and_load_dataframe(h5_paths, folder) df = pd.read_parquet(folder) From ddfb16c74b77c48e67289935503a50d4c092dcf8 Mon Sep 17 00:00:00 2001 From: zain-sohail <10053031+zain-sohail@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:45:58 +0000 Subject: [PATCH 173/300] Update benchmark targets --- benchmarks/benchmark_targets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/benchmark_targets.yaml b/benchmarks/benchmark_targets.yaml index 9b0160a5..1b8e4939 100644 --- a/benchmarks/benchmark_targets.yaml +++ b/benchmarks/benchmark_targets.yaml @@ -1,7 +1,7 @@ binning_1d: 3.017609174399999 binning_4d: 9.210316116800005 inv_dfield: 5.196141159999996 -loader_compute_flash: 0.014761656549995904 +loader_compute_flash: 0.00917599634999533 loader_compute_mpes: 0.015864623800007395 loader_compute_sxp: 0.006027440450000654 workflow_1d: 17.0553120846 From 019bf40dd8b0b826c58894630505133d1f83b280 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 23 Jul 2024 20:19:22 +0200 Subject: [PATCH 174/300] remove invalid channels --- sed/loader/flash/dataframe.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index d9a3ace0..ab1f7270 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -277,19 +277,41 @@ def df_train(self) -> pd.DataFrame: # All the channels are concatenated to a single DataFrame return pd.concat(series, axis=1) - def validate_channel_keys(self) -> None: + def validate_channel_keys(self, remove_invalid: bool = True) -> None: """ - Validates if the index and dataset keys for all channels in config exist in the h5 file. + Validates if the index and dataset keys for all channels in the config exist in the h5 file. + Prints a warning and removes the channel from the config if the keys do not exist, + based on the remove_invalid flag. - Raises: - KeyError: If the index or dataset keys do not exist in the file. + Args: + remove_invalid (bool): If True, removes channels with invalid keys from the config. + If False, prints an error message without removing the channels. """ + channels_to_remove = set() + for channel in self._config["channels"]: index_key, dataset_key = self.get_index_dataset_key(channel) if index_key not in self.h5_file: - raise KeyError(f"pd.Index key '{index_key}' doesn't exist in the file.") + Warning( + f"Key '{index_key}' for channel '{channel}' doesn't exist in the file.", + ) + channels_to_remove.add(channel) if dataset_key not in self.h5_file: - raise KeyError(f"Dataset key '{dataset_key}' doesn't exist in the file.") + Warning( + f"Key '{dataset_key}' for channel '{channel}' doesn't exist in the file.", + ) + channels_to_remove.add(channel) + + if remove_invalid: + for channel in channels_to_remove: + del self._config["channels"][channel] + else: + for channel in channels_to_remove: + KeyError( + "Index or dataset key for channel '{channel}' does not exist in H5 file." + "Either remove_invalid should be set to True or it should be " + "removed from the config.", + ) @property def df(self) -> pd.DataFrame: From 80da01d1ab94c69e8dab2c3f816540d88578a4ea Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 25 Jul 2024 20:43:15 +0200 Subject: [PATCH 175/300] create exception and use it to find invalid files --- sed/loader/flash/buffer_handler.py | 48 +++++++++++++++++++++++++----- sed/loader/flash/dataframe.py | 47 +++++++++-------------------- sed/loader/flash/loader.py | 24 +++++++++++---- sed/loader/flash/utils.py | 16 ++++++++-- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index d1a3c24e..8cacb48c 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -12,6 +12,7 @@ from sed.loader.flash.dataframe import DataFrameCreator from sed.loader.flash.utils import get_channels from sed.loader.flash.utils import get_dtypes +from sed.loader.flash.utils import InvalidFileError from sed.loader.utils import get_parquet_metadata from sed.loader.utils import split_dld_time_from_sector_id @@ -33,7 +34,14 @@ class BufferFilePaths: } """ - def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: + def __init__( + self, + config: dict, + h5_paths: list[Path], + folder: Path, + suffix: str, + remove_invalid_files: bool, + ) -> None: """Initializes the BufferFilePaths. Args: @@ -45,8 +53,18 @@ def __init__(self, h5_paths: list[Path], folder: Path, suffix: str) -> None: folder = folder / "buffer" folder.mkdir(parents=True, exist_ok=True) - # a list of file sets containing the paths to the raw, electron and timed buffer files - self._file_paths = [ + if remove_invalid_files: + h5_paths = self.remove_invalid_files(config, h5_paths) + + self._file_paths = self._create_file_paths(h5_paths, folder, suffix) + + def _create_file_paths( + self, + h5_paths: list[Path], + folder: Path, + suffix: str, + ) -> list[dict[str, Path]]: + return [ { "raw": h5_path, **{typ: folder / f"{typ}_{h5_path.stem}{suffix}" for typ in DF_TYP}, @@ -71,6 +89,18 @@ def file_sets_to_process(self, force_recreate: bool = False) -> list[dict[str, P return self._file_paths return [file_set for file_set in self if any(not file_set[key].exists() for key in DF_TYP)] + def remove_invalid_files(self, config, h5_paths: list[Path]) -> list[Path]: + valid_h5_paths = [] + for h5_path in h5_paths: + try: + dfc = DataFrameCreator(config_dataframe=config, h5_path=h5_path) + dfc.validate_channel_keys() + valid_h5_paths.append(h5_path) + except InvalidFileError as e: + print(f"Skipping invalid file: {h5_path.stem}\n{e}") + + return valid_h5_paths + class BufferHandler: """ @@ -157,6 +187,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: df_timed = df[self.fill_channels].loc[:, :, 0] dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) + print(f"Processed {paths['raw'].stem}") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ @@ -173,9 +204,8 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: if debug: for file_set in file_sets: self._save_buffer_file(file_set) - print(f"Processed {file_set['raw'].stem}") else: - Parallel(n_jobs=n_cores, verbose=10)( + Parallel(n_jobs=-2, verbose=10, prefer="threads")( delayed(self._save_buffer_file)(file_set) for file_set in file_sets ) @@ -219,7 +249,10 @@ def _get_dataframes(self) -> None: self.df[typ] = df self.metadata.update({"file_statistics": file_stats, "filling": filling}) # Correct the 3-bit shift which encodes the detector ID in the 8s time - if self._config.get("split_sector_id_from_dld_time", False): + if ( + self._config.get("split_sector_id_from_dld_time", False) + and self._config.get("tof_column", "dldTimeSteps") in self.df["electron"].columns + ): self.df["electron"], meta = split_dld_time_from_sector_id( self.df["electron"], config=self._config, @@ -233,6 +266,7 @@ def process_and_load_dataframe( force_recreate: bool = False, suffix: str = "", debug: bool = False, + remove_invalid_files: bool = False, ) -> tuple[dd.DataFrame, dd.DataFrame]: """ Runs the buffer file creation process. @@ -249,7 +283,7 @@ def process_and_load_dataframe( Returns: Tuple[dd.DataFrame, dd.DataFrame]: The electron and timed dataframes. """ - self.fp = BufferFilePaths(h5_paths, folder, suffix) + self.fp = BufferFilePaths(self._config, h5_paths, folder, suffix, remove_invalid_files) if not force_recreate: schema_set = set( diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index ab1f7270..37308a8b 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -13,6 +13,7 @@ import pandas as pd from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import InvalidFileError class DataFrameCreator: @@ -148,16 +149,16 @@ def df_electron(self) -> pd.DataFrame: Returns: pd.DataFrame: The pandas DataFrame for the 'per_electron' channel's data. """ - offset = self._config.get("ubid_offset", 5) # 5 is the default value - # Here we get the multi-index and the indexer to sort the data - index, indexer = self.pulse_index(offset) - # Get the relevant channels and their slice index channels = get_channels(self._config, "per_electron") if channels == []: return pd.DataFrame() slice_index = [self._config["channels"][channel].get("slice", None) for channel in channels] + offset = self._config.get("ubid_offset", 5) # 5 is the default value + # Here we get the multi-index and the indexer to sort the data + index, indexer = self.pulse_index(offset) + # First checking if dataset keys are the same for all channels # because DLD at FLASH stores all channels in the same h5 dataset dataset_keys = [self.get_index_dataset_key(channel)[1] for channel in channels] @@ -277,41 +278,21 @@ def df_train(self) -> pd.DataFrame: # All the channels are concatenated to a single DataFrame return pd.concat(series, axis=1) - def validate_channel_keys(self, remove_invalid: bool = True) -> None: + def validate_channel_keys(self) -> None: """ Validates if the index and dataset keys for all channels in the config exist in the h5 file. - Prints a warning and removes the channel from the config if the keys do not exist, - based on the remove_invalid flag. - Args: - remove_invalid (bool): If True, removes channels with invalid keys from the config. - If False, prints an error message without removing the channels. + Raises: + InvalidFileError: If the index or dataset keys are missing in the h5 file. """ - channels_to_remove = set() - + invalid_channels = [] for channel in self._config["channels"]: index_key, dataset_key = self.get_index_dataset_key(channel) - if index_key not in self.h5_file: - Warning( - f"Key '{index_key}' for channel '{channel}' doesn't exist in the file.", - ) - channels_to_remove.add(channel) - if dataset_key not in self.h5_file: - Warning( - f"Key '{dataset_key}' for channel '{channel}' doesn't exist in the file.", - ) - channels_to_remove.add(channel) - - if remove_invalid: - for channel in channels_to_remove: - del self._config["channels"][channel] - else: - for channel in channels_to_remove: - KeyError( - "Index or dataset key for channel '{channel}' does not exist in H5 file." - "Either remove_invalid should be set to True or it should be " - "removed from the config.", - ) + if index_key not in self.h5_file or dataset_key not in self.h5_file: + invalid_channels.append(channel) + + if invalid_channels: + raise InvalidFileError(invalid_channels) @property def df(self) -> pd.DataFrame: diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 3426f4a1..8d9e00dc 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -269,10 +269,6 @@ def read_dataframe( ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, - detector: str = "", - force_recreate: bool = False, - processed_dir: str | Path = None, - debug: bool = False, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -289,7 +285,16 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. - **kwds: Additional keyword arguments passed to ``parse_metadata``. + **kwds: Additional keyword arguments. + Keyword Args: + detector (str, optional): The detector to use. Defaults to "". + force_recreate (bool, optional): Whether to force recreation of the buffer files. + Defaults to False. + processed_dir (str, optional): The directory to save the processed files. + Defaults to None. + debug (bool, optional): Whether to run buffer creation in serial. Defaults to False. + remove_invalid_files (bool, optional): Whether to exclude invalid files. + Defaults to False. Returns: tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame @@ -299,6 +304,14 @@ def read_dataframe( ValueError: If neither 'runs' nor 'files'/'raw_dir' is provided. FileNotFoundError: If the conversion fails for some files or no data is available. """ + detector = kwds.pop("detector", "") + force_recreate = kwds.pop("force_recreate", False) + processed_dir = kwds.pop("processed_dir", None) + debug = kwds.pop("debug", False) + remove_invalid_files = kwds.pop("remove_invalid_files", False) + + if len(kwds) > 0: + raise ValueError(f"Unexpected keyword arguments: {kwds.keys()}") t0 = time.time() self._initialize_dirs() @@ -341,6 +354,7 @@ def read_dataframe( force_recreate=force_recreate, suffix=detector, debug=debug, + remove_invalid_files=remove_invalid_files, ) if self.instrument == "wespe": diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 3d7843af..6eb2ac30 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -62,8 +62,9 @@ def get_channels( # Get the available channels excluding 'pulseId'. available_channels = list(channel_dict.keys()) - # raises error if not available, but necessary for pulse_index - available_channels.remove(PULSE_ALIAS) + # pulse alias is an index and should not be included in the list of channels. + if PULSE_ALIAS in available_channels: + available_channels.remove(PULSE_ALIAS) for format_ in formats: # Gather channels based on the specified format(s). @@ -108,3 +109,14 @@ def get_dtypes(config_dataframe: dict, df_cols: list) -> dict: except KeyError: dtypes[channel] = None return dtypes + + +class InvalidFileError(Exception): + """Raised when an H5 file is invalid due to missing keys defined in the config.""" + + def __init__(self, invalid_channels: list[str]): + self.invalid_channels = invalid_channels + super().__init__( + f"Channels not in file: {', '.join(invalid_channels)}. " + "If you are using the loader, set 'remove_invalid_files' to True to ignore these files", + ) From 4d1ce1e2d6a0135aa93265b376b0379cbd687dcb Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Fri, 26 Jul 2024 12:43:58 +0200 Subject: [PATCH 176/300] add exception when no valid files are available and fix testing --- sed/loader/flash/buffer_handler.py | 2 + tests/loader/flash/conftest.py | 16 ++++++ tests/loader/flash/test_buffer_handler.py | 61 ++++++++++++++++++++--- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 8cacb48c..563ce12f 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -221,6 +221,8 @@ def _get_dataframes(self) -> None: it pulse resolved (no longer electron resolved). If time_index is True, the timeIndex is calculated and set as the index (slow operation). """ + if not self.fp: + raise FileNotFoundError("Buffer files do not exist.") # Loop over the electron and timed dataframes file_stats = {} filling = {} diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 2bd40b75..8fa9c3b7 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -64,6 +64,22 @@ def fixture_h5_file_copy(tmp_path: Path) -> h5py.File: return h5py.File(copy_file_path, "r+") +@pytest.fixture(name="h5_file2_copy") +def fixture_h5_file2_copy(tmp_path: Path) -> h5py.File: + """Fixture providing a copy of an open h5 file. + + Returns: + h5py.File: The open h5 file copy. + """ + # Create a copy of the h5 file in a temporary directory + original_file_path = os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATHS[1]}") + copy_file_path = tmp_path / "copy2.h5" + shutil.copyfile(original_file_path, copy_file_path) + + # Open the copy in 'read-write' mode and return it + return h5py.File(copy_file_path, "r+") + + @pytest.fixture(name="h5_paths") def fixture_h5_paths() -> list[Path]: """Fixture providing a list of h5 file paths. diff --git a/tests/loader/flash/test_buffer_handler.py b/tests/loader/flash/test_buffer_handler.py index e879367c..3eb0e625 100644 --- a/tests/loader/flash/test_buffer_handler.py +++ b/tests/loader/flash/test_buffer_handler.py @@ -10,6 +10,7 @@ from sed.loader.flash.buffer_handler import BufferFilePaths from sed.loader.flash.buffer_handler import BufferHandler from sed.loader.flash.utils import get_channels +from sed.loader.flash.utils import InvalidFileError def create_parquet_dir(config: dict, folder: str) -> Path: @@ -44,7 +45,7 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: the checks with modified file name parameters. """ folder = create_parquet_dir(config, "get_files_to_read") - fp = BufferFilePaths(h5_paths, folder, "") + fp = BufferFilePaths(config, h5_paths, folder, suffix="", remove_invalid_files=False) # check that all files are to be read assert len(fp.file_sets_to_process()) == len(h5_paths) @@ -69,7 +70,7 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: bh._save_buffer_file(path) # check again for files to read and expect one less file - fp = BufferFilePaths(h5_paths, folder, "") + fp = BufferFilePaths(config, h5_paths, folder, suffix="", remove_invalid_files=False) # check that only one file is to be read assert len(fp.file_sets_to_process()) == len(h5_paths) - 1 @@ -81,7 +82,7 @@ def test_buffer_file_paths(config: dict, h5_paths: list[Path]) -> None: Path(path["timed"]).unlink() # Test for adding a suffix - fp = BufferFilePaths(h5_paths, folder, "suffix") + fp = BufferFilePaths(config, h5_paths, folder, "suffix", remove_invalid_files=False) # expected buffer paths with prefix and suffix for typ in ["electron", "timed"]: @@ -188,9 +189,22 @@ def test_save_buffer_files_exception( config: dict, h5_paths: list[Path], h5_file_copy: File, + h5_file2_copy: File, tmp_path: Path, ) -> None: - """Test function to verify exception handling when running code in parallel.""" + """Test function to verify exception handling in the BufferHandler's + 'process_and_load_dataframe' method. The test checks for exceptions raised due to missing + channels in the configuration and empty datasets. + Test Steps: + - Create a directory structure for storing buffer files and initialize the BufferHandler. + - Check for an exception when a channel is missing in the configuration. + - Create an empty dataset in the HDF5 file to simulate an invalid file scenario. + - Check for an expected error related to the missing index dataset that invalidates the file. + - Check for an error when 'remove_invalid_files' is set to True and the file is invalid. + - Create an empty dataset in the second HDF5 file to simulate an invalid file scenario. + - Check for an error when 'remove_invalid_files' is set to True and the file is invalid. + - Check for an error when only a single file is provided, and the file is not buffered. + """ folder_parallel = create_parquet_dir(config, "save_buffer_files_exception") config_ = deepcopy(config) @@ -220,8 +234,8 @@ def test_save_buffer_files_exception( shape=0, ) - # expect key error because of missing index dataset - with pytest.raises(KeyError): + # expect invalid file error because of missing index dataset that invalidates entire file + with pytest.raises(InvalidFileError): bh = BufferHandler(config_) bh.process_and_load_dataframe( [tmp_path / "copy.h5"], @@ -230,6 +244,41 @@ def test_save_buffer_files_exception( force_recreate=True, ) + # create an empty dataset + h5_file2_copy.create_dataset( + name=channel_index_key, + shape=0, + ) + h5_file2_copy.create_dataset( + name=empty_dataset_key, + shape=0, + ) + + # if remove_invalid_files is True, the file should be removed and no error should be raised + bh = BufferHandler(config_) + try: + bh.process_and_load_dataframe( + [tmp_path / "copy.h5", tmp_path / "copy2.h5"], + folder_parallel, + debug=False, + force_recreate=True, + remove_invalid_files=True, + ) + except InvalidFileError: + assert ( + False + ), "InvalidFileError should not be raised when remove_invalid_files is set to True" + + # with only a single file, the file will not be buffered so a FileNotFoundError should be raised + with pytest.raises(FileNotFoundError): + bh.process_and_load_dataframe( + [tmp_path / "copy.h5"], + folder_parallel, + debug=False, + force_recreate=True, + remove_invalid_files=True, + ) + def test_get_filled_dataframe(config: dict, h5_paths: list[Path]) -> None: """Test function to verify the creation of a filled dataframe from the buffer files.""" From f8aab537f098bad7221064462eaf08ac6897a53c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 31 Jul 2024 20:17:52 +0200 Subject: [PATCH 177/300] implement suggested changes --- sed/loader/flash/buffer_handler.py | 11 +++++++---- sed/loader/flash/loader.py | 31 ++++++++++++++++-------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/sed/loader/flash/buffer_handler.py b/sed/loader/flash/buffer_handler.py index 563ce12f..b335dd01 100644 --- a/sed/loader/flash/buffer_handler.py +++ b/sed/loader/flash/buffer_handler.py @@ -9,6 +9,7 @@ from joblib import Parallel from sed.core.dfops import forward_fill_lazy +from sed.core.logging import setup_logging from sed.loader.flash.dataframe import DataFrameCreator from sed.loader.flash.utils import get_channels from sed.loader.flash.utils import get_dtypes @@ -19,6 +20,8 @@ DF_TYP = ["electron", "timed"] +logger = setup_logging(__name__) + class BufferFilePaths: """ @@ -97,7 +100,7 @@ def remove_invalid_files(self, config, h5_paths: list[Path]) -> list[Path]: dfc.validate_channel_keys() valid_h5_paths.append(h5_path) except InvalidFileError as e: - print(f"Skipping invalid file: {h5_path.stem}\n{e}") + logger.info(f"Skipping invalid file: {h5_path.stem}\n{e}") return valid_h5_paths @@ -187,7 +190,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: df_timed = df[self.fill_channels].loc[:, :, 0] dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) - print(f"Processed {paths['raw'].stem}") + logger.debug(f"Processed {paths['raw'].stem}") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ @@ -198,14 +201,14 @@ def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: debug (bool): Flag to enable debug mode, which serializes the creation. """ file_sets = self.fp.file_sets_to_process(force_recreate) - print(f"Reading files: {len(file_sets)} new files of {len(self.fp)} total.") + logger.info(f"Reading files: {len(file_sets)} new files of {len(self.fp)} total.") n_cores = min(len(file_sets), self.n_cores) if n_cores > 0: if debug: for file_set in file_sets: self._save_buffer_file(file_set) else: - Parallel(n_jobs=-2, verbose=10, prefer="threads")( + Parallel(n_jobs=n_cores, verbose=10)( delayed(self._save_buffer_file)(file_set) for file_set in file_sets ) diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 8d9e00dc..1e9752eb 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -203,9 +203,10 @@ def get_elapsed_time(self, fids: Sequence[int] = None, **kwds) -> float | list[f Args: fids (Sequence[int]): A sequence of file IDs. Defaults to all files. - **kwds: - - runs: A sequence of run IDs. Takes precedence over fids. - - aggregate: Whether to return the sum of the elapsed times across + + Keyword Args: + runs: A sequence of run IDs. Takes precedence over fids. + aggregate: Whether to return the sum of the elapsed times across the specified files or the elapsed time for each file. Defaults to True. Returns: @@ -285,16 +286,17 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. - **kwds: Additional keyword arguments. - Keyword Args: - detector (str, optional): The detector to use. Defaults to "". - force_recreate (bool, optional): Whether to force recreation of the buffer files. - Defaults to False. - processed_dir (str, optional): The directory to save the processed files. - Defaults to None. - debug (bool, optional): Whether to run buffer creation in serial. Defaults to False. - remove_invalid_files (bool, optional): Whether to exclude invalid files. - Defaults to False. + + Keyword Args: + detector (str, optional): The detector to use. Defaults to "". + force_recreate (bool, optional): Whether to force recreation of the buffer files. + Defaults to False. + processed_dir (str, optional): The directory to save the processed files. + Defaults to None. + debug (bool, optional): Whether to run buffer creation in serial. Defaults to False. + remove_invalid_files (bool, optional): Whether to exclude invalid files. + Defaults to False. + scicat_token (str, optional): The scicat token to use for fetching metadata. Returns: tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame @@ -309,6 +311,7 @@ def read_dataframe( processed_dir = kwds.pop("processed_dir", None) debug = kwds.pop("debug", False) remove_invalid_files = kwds.pop("remove_invalid_files", False) + scicat_token = kwds.pop("scicat_token", None) if len(kwds) > 0: raise ValueError(f"Unexpected keyword arguments: {kwds.keys()}") @@ -360,7 +363,7 @@ def read_dataframe( if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) - self.metadata.update(self.parse_metadata(**kwds) if collect_metadata else {}) + self.metadata.update(self.parse_metadata(scicat_token) if collect_metadata else {}) self.metadata.update(bh.metadata) print(f"loading complete in {time.time() - t0: .2f} s") From d305f192d8745dfe4786635b27f982a64775b980 Mon Sep 17 00:00:00 2001 From: "M. Zain Sohail" Date: Sun, 11 Aug 2024 23:30:37 +0200 Subject: [PATCH 178/300] let code run even if no train channels exist --- sed/loader/flash/dataframe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 37308a8b..1dc782d3 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -243,6 +243,8 @@ def df_train(self) -> pd.DataFrame: series = [] # Get the relevant channel names channels = get_channels(self._config, "per_train") + if channels == []: + return pd.DataFrame() # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned From 779b594acf13700f011ed9872badbb8d7bf76c80 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 14 Aug 2024 09:44:14 +0200 Subject: [PATCH 179/300] limit pynxtools --- poetry.lock | 8 ++++---- pyproject.toml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 84445a96..a1aaefe1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3480,13 +3480,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.6.0" +version = "0.5.2" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.6.0-py3-none-any.whl", hash = "sha256:75ee95196244ae3234480a16d7e9248049b23548ade69a6658d7fbb128529e6f"}, - {file = "pynxtools-0.6.0.tar.gz", hash = "sha256:ebb394507d0580aeb447767f295027921a4336476cce359a5afa5a604a3d91c2"}, + {file = "pynxtools-0.5.2-py3-none-any.whl", hash = "sha256:4d9abb366cf05a7fc85e1789de523b4836dac1c02031e94ac6de82d64c7d72f3"}, + {file = "pynxtools-0.5.2.tar.gz", hash = "sha256:717d11d0ff0e3ad07bce26fe4d2014fe48725fee3b0f77cf98ebc5e83acbfbb7"}, ] [package.dependencies] @@ -5060,4 +5060,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "d9910f0862cce403f33ec38bc2a930e2d31e682882c9c4cbcfe933f1ea0b16ee" +content-hash = "265a1dbea287706d3c38523717243712ed11ab3d8ffa0d8ac55cf3fa29e7a635" diff --git a/pyproject.toml b/pyproject.toml index ffb6db79..2ad3f4f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,8 @@ numba = ">=0.55.1" numpy = ">=1.18, <2.0" pandas = ">=1.4.1" psutil = ">=5.9.0" -pynxtools-mpes = ">=0.1.1" -pynxtools = ">=0.5.1" +pynxtools-mpes = "^0.1.1" +pynxtools = "^0.5.1" pyyaml = ">=6.0.0" scipy = ">=1.8.0" symmetrize = ">=0.5.5" From 2b6f96eeeebc54484fa6314e6fd310a3b10fcfae Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 21 Aug 2024 17:46:24 +0200 Subject: [PATCH 180/300] filter out all negative pulse values as they are invalid --- sed/loader/flash/dataframe.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 1dc782d3..80485a56 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -181,20 +181,10 @@ def df_electron(self) -> pd.DataFrame: } dataframe = pd.concat(series, axis=1) - # after offset, the negative pulse values are dropped as they are not valid - drop_vals = np.arange(-offset, 0) - - # Few things happen here: - # Drop all NaN values like while creating the multiindex - # if necessary, the data is sorted with [indexer] - # pd.MultiIndex is set - # Finally, the offset values are dropped - return ( - dataframe.dropna() - .iloc[indexer] - .set_index(index) - .drop(index=drop_vals, level="pulseId", errors="ignore") - ) + # NaN values dropped, data sorted with [indexer] if necessary, and the MultiIndex is set + dataframe = dataframe.dropna().iloc[indexer].set_index(index) + # after offset, all the negative pulse values are dropped as they are not valid + return dataframe[dataframe.index.get_level_values("pulseId") >= 0] @property def df_pulse(self) -> pd.DataFrame: @@ -243,8 +233,6 @@ def df_train(self) -> pd.DataFrame: series = [] # Get the relevant channel names channels = get_channels(self._config, "per_train") - if channels == []: - return pd.DataFrame() # For each channel, a pd.Series is created and appended to the list for channel in channels: # train_index and (sliced) data is returned From a890d5df965ed140b6262c6bae18d428b015c929 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 21 Aug 2024 18:16:00 +0200 Subject: [PATCH 181/300] filter out in final df --- sed/loader/flash/dataframe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 80485a56..887cb9dd 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -182,9 +182,7 @@ def df_electron(self) -> pd.DataFrame: dataframe = pd.concat(series, axis=1) # NaN values dropped, data sorted with [indexer] if necessary, and the MultiIndex is set - dataframe = dataframe.dropna().iloc[indexer].set_index(index) - # after offset, all the negative pulse values are dropped as they are not valid - return dataframe[dataframe.index.get_level_values("pulseId") >= 0] + return dataframe.dropna().iloc[indexer].set_index(index) @property def df_pulse(self) -> pd.DataFrame: @@ -297,4 +295,6 @@ def df(self) -> pd.DataFrame: self.validate_channel_keys() # been tested with merge, join and concat # concat offers best performance, almost 3 times faster - return pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() + df = pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() + # all the negative pulse values are dropped as they are invalid + return df[df.index.get_level_values("pulseId") >= 0] From bda9e1090ded22add354221d3741bd190a7b1fd5 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 18 Jul 2024 23:52:39 +0200 Subject: [PATCH 182/300] replace prints by logging --- sed/core/config.py | 36 +++++++++++------- sed/core/processor.py | 85 ++++++++++++++++++++++--------------------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/sed/core/config.py b/sed/core/config.py index f359c426..8de69c1f 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -4,6 +4,7 @@ import copy import json +import logging import os import platform from importlib.util import find_spec @@ -12,10 +13,15 @@ import yaml from platformdirs import user_config_path +from sed.core.logging import setup_logging + package_dir = os.path.dirname(find_spec("sed").origin) USER_CONFIG_PATH = user_config_path(appname="sed", appauthor="OpenCOMPES", ensure_exists=True) +# Configure logging +logger = setup_logging(__name__) + def parse_config( config: dict | str = None, @@ -57,6 +63,11 @@ def parse_config( Returns: dict: Loaded and possibly completed config dictionary. """ + if verbose: + logger.handlers[0].setLevel(logging.INFO) + else: + logger.handlers[0].setLevel(logging.WARNING) + if config is None: config = {} @@ -64,8 +75,7 @@ def parse_config( config_dict = copy.deepcopy(config) else: config_dict = load_config(config) - if verbose: - print(f"Configuration loaded from: [{str(Path(config).resolve())}]") + logger.info(f"Configuration loaded from: [{str(Path(config).resolve())}]") folder_dict: dict = None if isinstance(folder_config, dict): @@ -75,8 +85,7 @@ def parse_config( folder_config = "./sed_config.yaml" if Path(folder_config).exists(): folder_dict = load_config(folder_config) - if verbose: - print(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]") + logger.info(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]") user_dict: dict = None if isinstance(user_config, dict): @@ -88,8 +97,7 @@ def parse_config( ) if Path(user_config).exists(): user_dict = load_config(user_config) - if verbose: - print(f"User config loaded from: [{str(Path(user_config).resolve())}]") + logger.info(f"User config loaded from: [{str(Path(user_config).resolve())}]") system_dict: dict = None if isinstance(system_config, dict): @@ -106,15 +114,13 @@ def parse_config( ) if Path(system_config).exists(): system_dict = load_config(system_config) - if verbose: - print(f"System config loaded from: [{str(Path(system_config).resolve())}]") + logger.info(f"System config loaded from: [{str(Path(system_config).resolve())}]") if isinstance(default_config, dict): default_dict = copy.deepcopy(default_config) else: default_dict = load_config(default_config) - if verbose: - print(f"Default config loaded from: [{str(Path(default_config).resolve())}]") + logger.info(f"Default config loaded from: [{str(Path(default_config).resolve())}]") if folder_dict is not None: config_dict = complete_dictionary( @@ -154,9 +160,9 @@ def load_config(config_path: str) -> dict: """ config_file = Path(config_path) if not config_file.is_file(): - raise FileNotFoundError( - f"could not find the configuration file: {config_file}", - ) + error_message = f"could not find the configuration file: {config_file}" + logger.error(error_message) + raise FileNotFoundError(error_message) if config_file.suffix == ".json": with open(config_file, encoding="utf-8") as stream: @@ -165,7 +171,9 @@ def load_config(config_path: str) -> dict: with open(config_file, encoding="utf-8") as stream: config_dict = yaml.safe_load(stream) else: - raise TypeError("config file must be of type json or yaml!") + error_message = "config file must be of type json or yaml!" + logger.error(error_message) + raise TypeError(error_message) return config_dict diff --git a/sed/core/processor.py b/sed/core/processor.py index 5644a1b3..3371a37d 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -27,6 +27,7 @@ from sed.core.dfops import add_time_stamped_data from sed.core.dfops import apply_filter from sed.core.dfops import apply_jitter +from sed.core.logging import setup_logging from sed.core.metadata import MetaHandler from sed.diagnostics import grid_histogram from sed.io import to_h5 @@ -39,6 +40,9 @@ N_CPU = psutil.cpu_count() +# Configure logging +logger = setup_logging(__name__) + class SedProcessor: """Processor class of sed. Contains wrapper functions defining a work flow for data @@ -672,7 +676,7 @@ def save_splinewarp( }, } save_config(config, filename, overwrite) - print(f'Saved momentum correction parameters to "{filename}".') + logger.info(f'Saved momentum correction parameters to "{filename}".') # 4. Pose corrections. Provide interactive interface for correcting # scaling, shift and rotation @@ -763,7 +767,7 @@ def save_transformations( }, } save_config(config, filename, overwrite) - print(f'Saved momentum transformation parameters to "{filename}".') + logger.info(f'Saved momentum transformation parameters to "{filename}".') # 5. Apply the momentum correction to the dataframe def apply_momentum_correction( @@ -795,7 +799,7 @@ def apply_momentum_correction( if self._dataframe is not None: if verbose: - print("Adding corrected X/Y columns to dataframe:") + logger.info("Adding corrected X/Y columns to dataframe:") df, metadata = self.mc.apply_corrections( df=self._dataframe, verbose=verbose, @@ -828,10 +832,10 @@ def apply_momentum_correction( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if self.verbose: - print(self._dataframe) + logger.info(self._dataframe) # Momentum calibration work flow # 1. Calculate momentum calibration @@ -916,7 +920,7 @@ def save_momentum_calibration( config = {"momentum": {"calibration": calibration}} save_config(config, filename, overwrite) - print(f"Saved momentum calibration parameters to {filename}") + logger.info(f"Saved momentum calibration parameters to {filename}") # 2. Apply correction and calibration to the dataframe def apply_momentum_calibration( @@ -946,8 +950,7 @@ def apply_momentum_calibration( y_column = self._config["dataframe"]["y_column"] if self._dataframe is not None: - if verbose: - print("Adding kx/ky columns to dataframe:") + logger.info("Adding kx/ky columns to dataframe:") df, metadata = self.mc.append_k_axis( df=self._dataframe, calibration=calibration, @@ -980,10 +983,10 @@ def apply_momentum_calibration( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if self.verbose: - print(self._dataframe) + logger.info(self._dataframe) # Energy correction workflow # 1. Adjust the energy correction parameters @@ -1018,9 +1021,7 @@ def adjust_energy_correction( **kwds: Keyword parameters passed to ``EnergyCalibrator.adjust_energy_correction()``. """ if self._pre_binned is None: - print( - "Pre-binned data not present, binning using defaults from config...", - ) + logger.warn("Pre-binned data not present, binning using defaults from config...") self._pre_binned = self.pre_binning() self.ec.adjust_energy_correction( @@ -1064,7 +1065,7 @@ def save_energy_correction( config = {"energy": {"correction": correction}} save_config(config, filename, overwrite) - print(f"Saved energy correction parameters to {filename}") + logger.info(f"Saved energy correction parameters to {filename}") # 2. Apply energy correction to dataframe def apply_energy_correction( @@ -1094,7 +1095,7 @@ def apply_energy_correction( if self._dataframe is not None: if verbose: - print("Applying energy correction to dataframe...") + logger.info("Applying energy correction to dataframe...") df, metadata = self.ec.apply_energy_correction( df=self._dataframe, correction=correction, @@ -1120,10 +1121,10 @@ def apply_energy_correction( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if verbose: - print(self._dataframe) + logger.info(self._dataframe) # Energy calibrator workflow # 1. Load and normalize data @@ -1266,7 +1267,7 @@ def find_bias_peaks( mode=mode, radius=radius, ) - print(self.ec.featranges) + logger.info(self.ec.featranges) try: self.ec.feature_extract(peak_window=peak_window) self.ec.view( @@ -1419,7 +1420,7 @@ def save_energy_calibration( config = {"energy": {"calibration": calibration}} save_config(config, filename, overwrite) - print(f'Saved energy calibration parameters to "{filename}".') + logger.info(f'Saved energy calibration parameters to "{filename}".') # 4. Apply energy calibration to the dataframe def append_energy_axis( @@ -1455,7 +1456,7 @@ def append_energy_axis( if self._dataframe is not None: if verbose: - print("Adding energy column to dataframe:") + logger.info("Adding energy column to dataframe:") df, metadata = self.ec.append_energy_axis( df=self._dataframe, calibration=calibration, @@ -1485,10 +1486,10 @@ def append_energy_axis( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if verbose: - print(self._dataframe) + logger.info(self._dataframe) def add_energy_offset( self, @@ -1533,7 +1534,7 @@ def add_energy_offset( ) if self.dataframe is not None: if verbose: - print("Adding energy offset to dataframe:") + logger.info("Adding energy offset to dataframe:") df, metadata = self.ec.add_offsets( df=self._dataframe, constant=constant, @@ -1568,9 +1569,9 @@ def add_energy_offset( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) elif verbose: - print(self._dataframe) + logger.info(self._dataframe) def save_energy_offset( self, @@ -1595,7 +1596,7 @@ def save_energy_offset( config = {"energy": {"offsets": self.ec.offsets}} save_config(config, filename, overwrite) - print(f'Saved energy offset parameters to "{filename}".') + logger.info(f'Saved energy offset parameters to "{filename}".') def append_tof_ns_axis( self, @@ -1623,7 +1624,7 @@ def append_tof_ns_axis( if self._dataframe is not None: if verbose: - print("Adding time-of-flight column in nanoseconds to dataframe:") + logger.info("Adding time-of-flight column in nanoseconds to dataframe.") # TODO assert order of execution through metadata df, metadata = self.ec.append_tof_ns_axis( @@ -1647,10 +1648,10 @@ def append_tof_ns_axis( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if verbose: - print(self._dataframe) + logger.info(self._dataframe) def align_dld_sectors( self, @@ -1677,7 +1678,7 @@ def align_dld_sectors( if self._dataframe is not None: if verbose: - print("Aligning 8s sectors of dataframe") + logger.info("Aligning 8s sectors of dataframe") # TODO assert order of execution through metadata df, metadata = self.ec.align_dld_sectors( @@ -1703,10 +1704,10 @@ def align_dld_sectors( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if verbose: - print(self._dataframe) + logger.info(self._dataframe) # Delay calibration function def calibrate_delay_axis( @@ -1740,7 +1741,7 @@ def calibrate_delay_axis( if self._dataframe is not None: if verbose: - print("Adding delay column to dataframe:") + logger.info("Adding delay column to dataframe:") if delay_range is None and datafile is None: if len(self.dc.calibration) == 0: @@ -1779,10 +1780,10 @@ def calibrate_delay_axis( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if self.verbose: - print(self._dataframe) + logger.info(self._dataframe) def save_delay_calibration( self, @@ -1864,7 +1865,7 @@ def add_delay_offset( if self.dataframe is not None: if verbose: - print("Adding delay offset to dataframe:") + logger.info("Adding delay offset to dataframe:") df, metadata = self.dc.add_offsets( df=self._dataframe, constant=constant, @@ -1900,10 +1901,10 @@ def add_delay_offset( else: raise ValueError("No dataframe loaded!") if preview: - print(self._dataframe.head(10)) + logger.info(self._dataframe.head(10)) else: if verbose: - print(self._dataframe) + logger.info(self._dataframe) def save_delay_offsets( self, @@ -1932,7 +1933,7 @@ def save_delay_offsets( }, } save_config(config, filename, overwrite) - print(f'Saved delay offset parameters to "{filename}".') + logger.info(f'Saved delay offset parameters to "{filename}".') def save_workflow_params( self, @@ -2012,6 +2013,7 @@ def add_jitter( metadata.append(col) # TODO: allow only appending if columns are not jittered yet self._attributes.add(metadata, "jittering", duplicate_policy="append") + logger.info(f"add_jitter: Added jitter to columns {cols}.") def add_time_stamped_data( self, @@ -2085,6 +2087,7 @@ def add_time_stamped_data( metadata.append(time_stamps) metadata.append(data) self._attributes.add(metadata, "time_stamped_data", duplicate_policy="append") + logger.info(f"add_time_stamped_data: Added time-stamped data as column {dest_column}.") def pre_binning( self, @@ -2264,9 +2267,7 @@ def compute( if normalize_to_acquisition_time: if isinstance(normalize_to_acquisition_time, str): axis = normalize_to_acquisition_time - print( - f"Calculate normalization histogram for axis '{axis}'...", - ) + logger.info(f"Calculate normalization histogram for axis '{axis}'...") self._normalization_histogram = self.get_normalization_histogram( axis=axis, df_partitions=df_partitions, From f824badc40e1811d2849c623e44c0646041debbc Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 18 Jul 2024 23:53:13 +0200 Subject: [PATCH 183/300] fixes for new matplotlib --- sed/calibrator/momentum.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index da70c55a..44f1def2 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -510,8 +510,8 @@ def update_point_pos( features[point_no][0] = point_x features[point_no][1] = point_y - markers[point_no].set_xdata(point_x) - markers[point_no].set_ydata(point_y) + markers[point_no].set_xdata([point_x]) + markers[point_no].set_ydata([point_y]) point_no_input = ipw.Dropdown( options=range(features.shape[0]), @@ -1509,10 +1509,10 @@ def update( k_distance: float, # noqa: ARG001 ): fig.canvas.draw_idle() - marker_a.set_xdata(point_a_x) - marker_a.set_ydata(point_a_y) - marker_b.set_xdata(point_b_x) - marker_b.set_ydata(point_b_y) + marker_a.set_xdata([point_a_x]) + marker_a.set_ydata([point_a_y]) + marker_b.set_xdata([point_b_x]) + marker_b.set_ydata([point_b_y]) point_a_input_x = ipw.IntText(point_a[0]) point_a_input_y = ipw.IntText(point_a[1]) From fbb0af27d304ff879dfc2a58309fb888e1754f22 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 11 Aug 2024 12:48:40 +0200 Subject: [PATCH 184/300] use base sed logger add call_logger to capture function calls in processor --- sed/core/config.py | 23 +++++++------ sed/core/logging.py | 75 ++++++++++++++++++++++++++++++------------ sed/core/processor.py | 31 ++++++++++++++++- sed/dataset/dataset.py | 5 +-- 4 files changed, 98 insertions(+), 36 deletions(-) diff --git a/sed/core/config.py b/sed/core/config.py index 8de69c1f..22e60fe5 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -4,7 +4,6 @@ import copy import json -import logging import os import platform from importlib.util import find_spec @@ -20,7 +19,7 @@ USER_CONFIG_PATH = user_config_path(appname="sed", appauthor="OpenCOMPES", ensure_exists=True) # Configure logging -logger = setup_logging(__name__) +logger = setup_logging("config") def parse_config( @@ -63,11 +62,6 @@ def parse_config( Returns: dict: Loaded and possibly completed config dictionary. """ - if verbose: - logger.handlers[0].setLevel(logging.INFO) - else: - logger.handlers[0].setLevel(logging.WARNING) - if config is None: config = {} @@ -75,7 +69,8 @@ def parse_config( config_dict = copy.deepcopy(config) else: config_dict = load_config(config) - logger.info(f"Configuration loaded from: [{str(Path(config).resolve())}]") + if verbose: + logger.info(f"Configuration loaded from: [{str(Path(config).resolve())}]") folder_dict: dict = None if isinstance(folder_config, dict): @@ -85,7 +80,8 @@ def parse_config( folder_config = "./sed_config.yaml" if Path(folder_config).exists(): folder_dict = load_config(folder_config) - logger.info(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]") + if verbose: + logger.info(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]") user_dict: dict = None if isinstance(user_config, dict): @@ -97,7 +93,8 @@ def parse_config( ) if Path(user_config).exists(): user_dict = load_config(user_config) - logger.info(f"User config loaded from: [{str(Path(user_config).resolve())}]") + if verbose: + logger.info(f"User config loaded from: [{str(Path(user_config).resolve())}]") system_dict: dict = None if isinstance(system_config, dict): @@ -114,13 +111,15 @@ def parse_config( ) if Path(system_config).exists(): system_dict = load_config(system_config) - logger.info(f"System config loaded from: [{str(Path(system_config).resolve())}]") + if verbose: + logger.info(f"System config loaded from: [{str(Path(system_config).resolve())}]") if isinstance(default_config, dict): default_dict = copy.deepcopy(default_config) else: default_dict = load_config(default_config) - logger.info(f"Default config loaded from: [{str(Path(default_config).resolve())}]") + if verbose: + logger.info(f"Default config loaded from: [{str(Path(default_config).resolve())}]") if folder_dict is not None: config_dict = complete_dictionary( diff --git a/sed/core/logging.py b/sed/core/logging.py index 8c4742a7..539f29b6 100644 --- a/sed/core/logging.py +++ b/sed/core/logging.py @@ -10,6 +10,8 @@ import os import sys from datetime import datetime +from functools import wraps +from typing import Callable # Default log directory DEFAULT_LOG_DIR = os.path.join(os.getcwd(), "logs") @@ -19,14 +21,17 @@ def setup_logging( name: str, - user_log_path: str = DEFAULT_LOG_DIR, + set_base_handler: bool = False, + user_log_path: str | None = None, ) -> logging.Logger: """ Configures and returns a logger with specified log levels for console and file handlers. Args: name (str): The name of the logger. - user_log_path (str): Path to the user-specific log directory. Defaults to DEFAULT_LOG_DIR. + base_name (str, optional): The name of the base logger. Defaults to "sed". + user_log_path (str, optional): Path to the user-specific log directory. + Defaults to DEFAULT_LOG_DIR. Returns: logging.Logger: The configured logger instance. @@ -34,11 +39,37 @@ def setup_logging( The logger will always write DEBUG level messages to a file located in the user's log directory, while the console log level can be adjusted based on the 'verbosity' parameter. """ - # Create logger - logger = logging.getLogger(name) + # Create base logger + base_logger = logging.getLogger("sed") + base_logger.setLevel(logging.DEBUG) # Set the minimum log level for the logger + if set_base_handler or not base_logger.hasHandlers(): + if base_logger.hasHandlers(): + base_logger.handlers.clear() + + # Determine log file path + if user_log_path is None: + user_log_path = DEFAULT_LOG_DIR + os.makedirs(user_log_path, exist_ok=True) + log_file = os.path.join(user_log_path, f"sed_{datetime.now().strftime('%Y-%m-%d')}.log") + + # Create file handler and set level to debug + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(FILE_VERBOSITY) + + # Create formatter for file + file_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s in %(filename)s:%(lineno)d", + ) + file_handler.setFormatter(file_formatter) + + # Add file handler to logger + base_logger.addHandler(file_handler) + + # create named logger + logger = base_logger.getChild(name) + if logger.hasHandlers(): logger.handlers.clear() - logger.setLevel(logging.INFO) # Set the minimum log level for the logger # Create console handler and set level console_handler = logging.StreamHandler(sys.stdout) @@ -51,24 +82,26 @@ def setup_logging( # Add console handler to logger logger.addHandler(console_handler) - # Determine log file path - os.makedirs(user_log_path, exist_ok=True) - log_file = os.path.join(user_log_path, f"sed_{datetime.now().strftime('%Y-%m-%d')}.log") + # Capture warnings with the logging system + logging.captureWarnings(True) - # Create file handler and set level to debug - file_handler = logging.FileHandler(log_file) - file_handler.setLevel(FILE_VERBOSITY) + return logger - # Create formatter for file - file_formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s in %(filename)s:%(lineno)d", - ) - file_handler.setFormatter(file_formatter) - # Add file handler to logger - logger.addHandler(file_handler) +def call_logger(logger: logging.Logger): + def log_call(func: Callable): + @wraps(func) + def new_func(*args, **kwargs): + saved_args = locals() + args_str = "" + for arg in saved_args["args"][1:]: + args_str += f"{arg}, " + for name, arg in saved_args["kwargs"].items(): + args_str += f"{name}={arg}, " + args_str = args_str.rstrip(", ") + logger.debug(f"Call {func.__name__}({args_str})") + return func(*args, **kwargs) - # Capture warnings with the logging system - logging.captureWarnings(True) + return new_func - return logger + return log_call diff --git a/sed/core/processor.py b/sed/core/processor.py index 3371a37d..29f52851 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -27,6 +27,7 @@ from sed.core.dfops import add_time_stamped_data from sed.core.dfops import apply_filter from sed.core.dfops import apply_jitter +from sed.core.logging import call_logger from sed.core.logging import setup_logging from sed.core.metadata import MetaHandler from sed.diagnostics import grid_histogram @@ -41,7 +42,7 @@ N_CPU = psutil.cpu_count() # Configure logging -logger = setup_logging(__name__) +logger = setup_logging("processor") class SedProcessor: @@ -67,6 +68,7 @@ class SedProcessor: **kwds: Keyword arguments passed to the reader. """ + @call_logger(logger) def __init__( self, metadata: dict = None, @@ -368,6 +370,7 @@ def cpy(self, path: str | list[str]) -> str | list[str]: return path + @call_logger(logger) def load( self, dataframe: pd.DataFrame | ddf.DataFrame = None, @@ -454,6 +457,7 @@ def load( duplicate_policy="merge", ) + @call_logger(logger) def filter_column( self, column: str, @@ -496,6 +500,7 @@ def filter_column( # Momentum calibration workflow # 1. Bin raw detector data for distortion correction + @call_logger(logger) def bin_and_load_momentum_calibration( self, df_partitions: int | Sequence[int] = 100, @@ -539,6 +544,7 @@ def bin_and_load_momentum_calibration( # 2. Generate the spline warp correction from momentum features. # Either autoselect features, or input features from view above. + @call_logger(logger) def define_features( self, features: np.ndarray = None, @@ -592,6 +598,7 @@ def define_features( # 3. Generate the spline warp correction from momentum features. # If no features have been selected before, use class defaults. + @call_logger(logger) def generate_splinewarp( self, use_center: bool = None, @@ -680,6 +687,7 @@ def save_splinewarp( # 4. Pose corrections. Provide interactive interface for correcting # scaling, shift and rotation + @call_logger(logger) def pose_adjustment( self, transformations: dict[str, Any] = None, @@ -737,6 +745,7 @@ def pose_adjustment( ) # 4a. Save pose adjustment parameters to config file. + @call_logger(logger) def save_transformations( self, filename: str = None, @@ -770,6 +779,7 @@ def save_transformations( logger.info(f'Saved momentum transformation parameters to "{filename}".') # 5. Apply the momentum correction to the dataframe + @call_logger(logger) def apply_momentum_correction( self, preview: bool = False, @@ -839,6 +849,7 @@ def apply_momentum_correction( # Momentum calibration work flow # 1. Calculate momentum calibration + @call_logger(logger) def calibrate_momentum_axes( self, point_a: np.ndarray | list[int] = None, @@ -923,6 +934,7 @@ def save_momentum_calibration( logger.info(f"Saved momentum calibration parameters to {filename}") # 2. Apply correction and calibration to the dataframe + @call_logger(logger) def apply_momentum_calibration( self, calibration: dict = None, @@ -990,6 +1002,7 @@ def apply_momentum_calibration( # Energy correction workflow # 1. Adjust the energy correction parameters + @call_logger(logger) def adjust_energy_correction( self, correction_type: str = None, @@ -1068,6 +1081,7 @@ def save_energy_correction( logger.info(f"Saved energy correction parameters to {filename}") # 2. Apply energy correction to dataframe + @call_logger(logger) def apply_energy_correction( self, correction: dict = None, @@ -1128,6 +1142,7 @@ def apply_energy_correction( # Energy calibrator workflow # 1. Load and normalize data + @call_logger(logger) def load_bias_series( self, binned_data: xr.DataArray | tuple[np.ndarray, np.ndarray, np.ndarray] = None, @@ -1221,6 +1236,7 @@ def load_bias_series( ) # 2. extract ranges and get peak positions + @call_logger(logger) def find_bias_peaks( self, ranges: list[tuple] | tuple, @@ -1294,6 +1310,7 @@ def find_bias_peaks( ) # 3. Fit the energy calibration relation + @call_logger(logger) def calibrate_energy_axis( self, ref_energy: float, @@ -1423,6 +1440,7 @@ def save_energy_calibration( logger.info(f'Saved energy calibration parameters to "{filename}".') # 4. Apply energy calibration to the dataframe + @call_logger(logger) def append_energy_axis( self, calibration: dict = None, @@ -1491,6 +1509,7 @@ def append_energy_axis( if verbose: logger.info(self._dataframe) + @call_logger(logger) def add_energy_offset( self, constant: float = None, @@ -1598,6 +1617,7 @@ def save_energy_offset( save_config(config, filename, overwrite) logger.info(f'Saved energy offset parameters to "{filename}".') + @call_logger(logger) def append_tof_ns_axis( self, preview: bool = False, @@ -1653,6 +1673,7 @@ def append_tof_ns_axis( if verbose: logger.info(self._dataframe) + @call_logger(logger) def align_dld_sectors( self, sector_delays: np.ndarray = None, @@ -1710,6 +1731,7 @@ def align_dld_sectors( logger.info(self._dataframe) # Delay calibration function + @call_logger(logger) def calibrate_delay_axis( self, delay_range: tuple[float, float] = None, @@ -1822,6 +1844,7 @@ def save_delay_calibration( } save_config(config, filename, overwrite) + @call_logger(logger) def add_delay_offset( self, constant: float = None, @@ -1963,6 +1986,7 @@ def save_workflow_params( except (ValueError, AttributeError, KeyError): pass + @call_logger(logger) def add_jitter( self, cols: list[str] = None, @@ -2015,6 +2039,7 @@ def add_jitter( self._attributes.add(metadata, "jittering", duplicate_policy="append") logger.info(f"add_jitter: Added jitter to columns {cols}.") + @call_logger(logger) def add_time_stamped_data( self, dest_column: str, @@ -2089,6 +2114,7 @@ def add_time_stamped_data( self._attributes.add(metadata, "time_stamped_data", duplicate_policy="append") logger.info(f"add_time_stamped_data: Added time-stamped data as column {dest_column}.") + @call_logger(logger) def pre_binning( self, df_partitions: int | Sequence[int] = 100, @@ -2136,6 +2162,7 @@ def pre_binning( **kwds, ) + @call_logger(logger) def compute( self, bins: int | dict | tuple | list[int] | list[np.ndarray] | list[tuple] = 100, @@ -2299,6 +2326,7 @@ def compute( return self._binned + @call_logger(logger) def get_normalization_histogram( self, axis: str = "delay", @@ -2463,6 +2491,7 @@ def view_event_histogram( **kwds, ) + @call_logger(logger) def save( self, faddr: str, diff --git a/sed/dataset/dataset.py b/sed/dataset/dataset.py index 9fb4a3d3..15bf8b4f 100644 --- a/sed/dataset/dataset.py +++ b/sed/dataset/dataset.py @@ -22,7 +22,7 @@ # Configure logging -logger = setup_logging(__name__) +logger = setup_logging("dataset") class DatasetsManager: @@ -51,7 +51,8 @@ def load_datasets_dict() -> dict: return parse_config( folder_config=DatasetsManager.json_path["folder"], - system_config=DatasetsManager.json_path["user"], + user_config=DatasetsManager.json_path["user"], + system_config={}, default_config=DatasetsManager.json_path["module"], verbose=False, ) From 469236d01ad281abfebf3d23c099361aef8c22a4 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 11 Aug 2024 13:03:35 +0200 Subject: [PATCH 185/300] add logging to mpes loader --- sed/loader/mpes/loader.py | 68 +++++++++++++++------------ tests/loader/mpes/test_mpes_loader.py | 23 +++++---- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index ea5ea7fc..42339ebf 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -23,8 +23,12 @@ import scipy.interpolate as sint from natsort import natsorted +from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader +# Configure logging +logger = setup_logging("mpes_loader") + def hdf5_to_dataframe( files: Sequence[str], @@ -74,17 +78,19 @@ def hdf5_to_dataframe( electron_channels.append(channel) column_names.append(name) else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + logger.warning( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " "Skipping the channel.", ) elif channel["format"] != "per_file": - raise ValueError( - f"Invalid 'format':{channel['format']} for channel {name}.", - ) + error_msg = f"Invalid 'format':{channel['format']} for channel {name}." + logger.error(error_msg) + raise ValueError(error_msg) if not electron_channels: - raise ValueError("No valid 'per_electron' channels found.") + error_msg = "No valid 'per_electron' channels found." + logger.error(error_msg) + raise ValueError(error_msg) if time_stamps: column_names.append(time_stamp_alias) @@ -127,8 +133,8 @@ def hdf5_to_dataframe( dataframe = ddf.from_delayed(delayeds) else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + logger.warning( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " "Skipping the channel.", ) @@ -184,17 +190,19 @@ def hdf5_to_timed_dataframe( electron_channels.append(channel) column_names.append(name) else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + logger.warning( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " "Skipping the channel.", ) elif channel["format"] != "per_file": - raise ValueError( - f"Invalid 'format':{channel['format']} for channel {name}.", - ) + error_msg = f"Invalid 'format':{channel['format']} for channel {name}." + logger.error(error_msg) + raise ValueError(error_msg) if not electron_channels: - raise ValueError("No valid 'per_electron' channels found.") + error_msg = "No valid 'per_electron' channels found." + logger.error(error_msg) + raise ValueError(error_msg) if time_stamps: column_names.append(time_stamp_alias) @@ -237,8 +245,8 @@ def hdf5_to_timed_dataframe( dataframe = ddf.from_delayed(delayeds) else: - print( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found.", + logger.warning( + f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " "Skipping the channel.", ) @@ -799,9 +807,9 @@ def gather_metadata( if metadata is None: metadata = {} - print("Gathering metadata from different locations") + logger.info("Gathering metadata from different locations") # Read events in with ms time stamps - print("Collecting time stamps...") + logger.info("Collecting time stamps...") (ts_from, ts_to) = self.get_start_and_end_time() metadata["timing"] = { @@ -819,7 +827,7 @@ def gather_metadata( if "file" not in metadata: # If already present, the value is assumed to be a dictionary metadata["file"] = {} - print("Collecting file metadata...") + logger.info("Collecting file metadata...") with h5py.File(files[0], "r") as h5file: for key, value in h5file.attrs.items(): key = key.replace("VSet", "V") @@ -829,7 +837,7 @@ def gather_metadata( os.path.realpath(files[0]), ) - print("Collecting data from the EPICS archive...") + logger.info("Collecting data from the EPICS archive...") # Get metadata from Epics archive if not present already epics_channels = self._config["metadata"]["epics_pvs"] @@ -850,23 +858,23 @@ def gather_metadata( except IndexError: metadata["file"][f"{channel}"] = np.nan - print( + logger.info( f"Data for channel {channel} doesn't exist for time {start}", ) except HTTPError as exc: - print( + logger.warning( f"Incorrect URL for the archive channel {channel}. " "Make sure that the channel name and file start and end times are " "correct.", ) - print("Error code: ", exc) + logger.warning(f"Error code: {exc}") except URLError as exc: - print( + logger.warning( f"Cannot access the archive URL for channel {channel}. " f"Make sure that you are within the FHI network." f"Skipping over channels {channels_missing}.", ) - print("Error code: ", exc) + logger.warning(f"Error code: {exc}") break # Determine the correct aperture_config @@ -901,7 +909,7 @@ def gather_metadata( metadata["instrument"]["analyzer"]["fa_shape"] = key break else: - print("Field aperture size not found.") + logger.warning("Field aperture size not found.") # get contrast aperture shape and size if self._config["metadata"]["ca_in_channel"] in metadata["file"]: @@ -917,7 +925,7 @@ def gather_metadata( metadata["instrument"]["analyzer"]["ca_shape"] = key break else: - print("Contrast aperture size not found.") + logger.warning("Contrast aperture size not found.") # Storing the lens modes corresponding to lens voltages. # Use lens voltages present in first lens_mode entry. @@ -938,7 +946,7 @@ def gather_metadata( metadata["instrument"]["analyzer"]["lens_mode"] = mode break else: - print( + logger.warning( "Lens mode for given lens voltages not found. " "Storing lens mode from the user, if provided.", ) @@ -953,13 +961,13 @@ def gather_metadata( metadata["instrument"]["analyzer"]["projection"] = "reciprocal" metadata["instrument"]["analyzer"]["scheme"] = "spatial dispersive" except IndexError: - print( + logger.warning( "Lens mode must have the form, '6kV_kmodem4.0_20VTOF_v3.sav'. " "Can't determine projection. " "Storing projection from the user, if provided.", ) except KeyError: - print( + logger.warning( "Lens mode not found. Can't determine projection. " "Storing projection from the user, if provided.", ) diff --git a/tests/loader/mpes/test_mpes_loader.py b/tests/loader/mpes/test_mpes_loader.py index 9d4513a0..936efe44 100644 --- a/tests/loader/mpes/test_mpes_loader.py +++ b/tests/loader/mpes/test_mpes_loader.py @@ -1,6 +1,7 @@ """Tests specific for Mpes loader""" from __future__ import annotations +import logging import os from copy import deepcopy from importlib.util import find_spec @@ -22,31 +23,33 @@ ) -def test_channel_not_found_warning(capsys) -> None: +def test_channel_not_found_warning(caplog) -> None: """Test if the mpes loader gives the correct warning if a channel cannot be found.""" ml = MpesLoader(config=config) - ml.read_dataframe(folders=test_data_dir) - captured = capsys.readouterr() - assert captured.out == "" + with caplog.at_level(logging.WARNING): + ml.read_dataframe(folders=test_data_dir) + assert not caplog.messages # modify per_file channel config_ = deepcopy(config) config_["dataframe"]["channels"]["sampleBias"]["dataset_key"] = "invalid" ml = MpesLoader(config=config_) - ml.read_dataframe(folders=test_data_dir) - captured = capsys.readouterr() - assert 'Entry "invalid" for channel "sampleBias" not found.' in captured.out + caplog.clear() + with caplog.at_level(logging.WARNING): + ml.read_dataframe(folders=test_data_dir) + assert 'Entry "invalid" for channel "sampleBias" not found.' in caplog.messages[0] # modify per_electron channel config_ = deepcopy(config) config_["dataframe"]["channels"]["X"]["dataset_key"] = "invalid" ml = MpesLoader(config=config_) - ml.read_dataframe(folders=test_data_dir) - captured = capsys.readouterr() - assert 'Entry "invalid" for channel "X" not found.' in captured.out + caplog.clear() + with caplog.at_level(logging.WARNING): + ml.read_dataframe(folders=test_data_dir) + assert 'Entry "invalid" for channel "X" not found.' in caplog.messages[0] def test_invalid_channel_format_raises() -> None: From 37feca19afbb12426f037af0737f90ac793c91cd Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 12 Aug 2024 21:02:55 +0200 Subject: [PATCH 186/300] add verbosity to loader interface --- sed/loader/base/loader.py | 7 ++++--- sed/loader/flash/loader.py | 22 ++++++++++++++++++++-- sed/loader/generic/loader.py | 3 ++- sed/loader/loader_interface.py | 4 +++- sed/loader/mpes/loader.py | 13 +++++++++++-- sed/loader/sxp/loader.py | 20 ++++++++++++++++++-- 6 files changed, 58 insertions(+), 11 deletions(-) diff --git a/sed/loader/base/loader.py b/sed/loader/base/loader.py index 4a9962d3..0f898e09 100644 --- a/sed/loader/base/loader.py +++ b/sed/loader/base/loader.py @@ -24,11 +24,10 @@ class BaseLoader(ABC): Args: config (dict, optional): Config dictionary. Defaults to None. - meta_handler (MetaHandler, optional): MetaHandler object. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ - # pylint: disable=too-few-public-methods - __name__ = "BaseLoader" supported_file_types: list[str] = [] @@ -36,12 +35,14 @@ class BaseLoader(ABC): def __init__( self, config: dict = None, + verbose: bool = True, ): self._config = config if config is not None else {} self.files: list[str] = [] self.runs: list[str] = [] self.metadata: dict[Any, Any] = {} + self.verbose = verbose @abstractmethod def read_dataframe( diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 1e9752eb..b44b3b82 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -12,36 +12,54 @@ import re import time from collections.abc import Sequence +from logging import INFO +from logging import WARNING from pathlib import Path import dask.dataframe as dd from natsort import natsorted +from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader from sed.loader.flash.buffer_handler import BufferHandler from sed.loader.flash.instruments import wespe_convert from sed.loader.flash.metadata import MetadataRetriever +# Configure logging +logger = setup_logging("flash_loader") + class FlashLoader(BaseLoader): """ The class generates multiindexed multidimensional pandas dataframes from the new FLASH dataformat resolved by both macro and microbunches alongside electrons. Only the read_dataframe (inherited and implemented) method is accessed by other modules. + + Args: + config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ __name__ = "flash" supported_file_types = ["h5"] - def __init__(self, config: dict) -> None: + def __init__(self, config: dict, verbose: bool = True) -> None: """ Initializes the FlashLoader. Args: config (dict): Configuration dictionary. + verbose (bool, optional): Option to print out diagnostic information. """ - super().__init__(config=config) + super().__init__(config=config, verbose=verbose) + + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) + self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof self.raw_dir: str = None self.processed_dir: str = None diff --git a/sed/loader/generic/loader.py b/sed/loader/generic/loader.py index 0444af0f..a7112a3e 100644 --- a/sed/loader/generic/loader.py +++ b/sed/loader/generic/loader.py @@ -19,7 +19,8 @@ class GenericLoader(BaseLoader): Args: config (dict, optional): Config dictionary. Defaults to None. - meta_handler (MetaHandler, optional): MetaHandler object. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ __name__ = "generic" diff --git a/sed/loader/loader_interface.py b/sed/loader/loader_interface.py index 6a636861..51d7d0e0 100644 --- a/sed/loader/loader_interface.py +++ b/sed/loader/loader_interface.py @@ -12,12 +12,14 @@ def get_loader( loader_name: str, config: dict = None, + verbose: bool = True, ) -> BaseLoader: """Helper function to get the loader object from it's given name. Args: loader_name (str): Name of the loader config (dict, optional): Configuration dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. Raises: ValueError: Raised if the loader cannot be found. @@ -41,7 +43,7 @@ def get_loader( spec = importlib.util.spec_from_file_location("loader.py", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - return module.LOADER(config=config) + return module.LOADER(config=config, verbose=verbose) def get_names_of_all_loaders() -> list[str]: diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 42339ebf..22df8a0a 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -10,6 +10,8 @@ import json import os from collections.abc import Sequence +from logging import INFO +from logging import WARNING from typing import Any from urllib.error import HTTPError from urllib.error import URLError @@ -565,7 +567,8 @@ class MpesLoader(BaseLoader): Args: config (dict, optional): Config dictionary. Defaults to None. - meta_handler (MetaHandler, optional): MetaHandler object. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ __name__ = "mpes" @@ -575,8 +578,14 @@ class MpesLoader(BaseLoader): def __init__( self, config: dict = None, + verbose: bool = True, ): - super().__init__(config=config) + super().__init__(config=config, verbose=verbose) + + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) self.read_timestamps = self._config.get("dataframe", {}).get( "read_timestamps", diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 10213594..e18a64f9 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -14,6 +14,8 @@ import time from collections.abc import Sequence from functools import reduce +from logging import INFO +from logging import WARNING from pathlib import Path import dask.dataframe as dd @@ -28,24 +30,38 @@ from pandas import Series from sed.core import dfops +from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader from sed.loader.utils import parse_h5_keys from sed.loader.utils import split_dld_time_from_sector_id +# Configure logging +logger = setup_logging("sxp_loader") + class SXPLoader(BaseLoader): """ The class generates multiindexed multidimensional pandas dataframes from the new SXP dataformat resolved by both macro and microbunches alongside electrons. Only the read_dataframe (inherited and implemented) method is accessed by other modules. + + Args: + config (dict): Config dictionary. + verbose (bool, optional): Option to print out diagnostic information. """ __name__ = "sxp" supported_file_types = ["h5"] - def __init__(self, config: dict) -> None: - super().__init__(config=config) + def __init__(self, config: dict, verbose: bool = True) -> None: + super().__init__(config=config, verbose=verbose) + + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) + self.multi_index = ["trainId", "pulseId", "electronId"] self.index_per_electron: MultiIndex = None self.index_per_pulse: MultiIndex = None From 2fbae5d7c6d20bc83f6b6e6d180b0cc5ac92d61c Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 12 Aug 2024 21:07:35 +0200 Subject: [PATCH 187/300] add logging to delay calibrator --- sed/calibrator/delay.py | 63 +++++++++++++++++++++++++---------------- sed/core/processor.py | 42 ++++++++++++++++----------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 666acbb7..d259bb60 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -5,6 +5,8 @@ from collections.abc import Sequence from copy import deepcopy from datetime import datetime +from logging import INFO +from logging import WARNING from typing import Any import dask.dataframe @@ -13,6 +15,10 @@ import pandas as pd from sed.core import dfops +from sed.core.logging import setup_logging + +# Configure logging +logger = setup_logging("delay") class DelayCalibrator: @@ -22,22 +28,33 @@ class DelayCalibrator: Args: config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ def __init__( self, config: dict = None, + verbose: bool = True, ) -> None: """Initialization of the DelayCalibrator class passes the config. Args: config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ if config is not None: self._config = config else: self._config = {} + self.verbose = verbose + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) + self.adc_column: str = self._config["dataframe"].get("adc_column", None) self.delay_column: str = self._config["dataframe"]["delay_column"] self.corrected_delay_column = self._config["dataframe"].get( @@ -61,7 +78,7 @@ def append_delay_axis( p1_key: str = None, p2_key: str = None, t0_key: str = None, - verbose: bool = True, + suppress_output: bool = False, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the delay axis to the events dataframe, by converting values from an analog-digital-converter (ADC). @@ -92,8 +109,7 @@ def append_delay_axis( Defaults to config["delay"]["p2_key"] t0_key (str, optional): hdf5 key for t0 value (mm). Defaults to config["delay"]["t0_key"] - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. + suppress_output (bool, optional): Option to suppress log output. Defaults to False. Raises: ValueError: Raised if delay parameters are not found in the file. @@ -126,11 +142,11 @@ def append_delay_axis( calibration["delay_range_mm"] = delay_range_mm else: # report usage of loaded parameters - if "creation_date" in calibration and verbose: + if "creation_date" in calibration and not suppress_output: datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using delay calibration parameters generated on {datestring}") + logger.info(f"Using delay calibration parameters generated on {datestring}") if adc_column is None: adc_column = self.adc_column @@ -166,8 +182,8 @@ def append_delay_axis( calibration["datafile"] = datafile calibration["delay_range_mm"] = (ret[0], ret[1]) calibration["time0"] = ret[2] - if verbose: - print(f"Extract delay range from file '{datafile}'.") + if not suppress_output: + logger.info(f"Extract delay range from file '{datafile}'.") else: raise NotImplementedError( "Not enough parameters for delay calibration.", @@ -179,8 +195,8 @@ def append_delay_axis( calibration["time0"], ), ) - if verbose: - print(f"Converted delay_range (ps) = {calibration['delay_range']}") + if not suppress_output: + logger.info(f"Converted delay_range (ps) = {calibration['delay_range']}") calibration["creation_date"] = datetime.now().timestamp() if "delay_range" in calibration.keys(): @@ -190,8 +206,8 @@ def append_delay_axis( calibration["adc_range"][1] - calibration["adc_range"][0] ) self.calibration = deepcopy(calibration) - if verbose: - print( + if not suppress_output: + logger.info( "Append delay axis using delay_range = " f"[{calibration['delay_range'][0]}, {calibration['delay_range'][1]}]" " and adc_range = " @@ -214,7 +230,7 @@ def add_offsets( preserve_mean: bool | Sequence[bool] = False, reductions: str | Sequence[str] = None, delay_column: str = None, - verbose: bool = True, + suppress_output: bool = False, ) -> tuple[dask.dataframe.DataFrame, dict]: """Apply an offset to the delay column based on a constant or other columns. @@ -234,8 +250,7 @@ def add_offsets( If None, the shift is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. delay_column (str, optional): Name of the column containing the delay values. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. + suppress_output (bool, optional): Option to suppress log output. Defaults to False. Returns: tuple[dask.dataframe.DataFrame, dict]: Dataframe with the shifted delay axis and @@ -299,11 +314,11 @@ def add_offsets( if flip_delay_axis: offsets["flip_delay_axis"] = flip_delay_axis - elif "creation_date" in offsets and verbose: + elif "creation_date" in offsets and not suppress_output: datestring = datetime.fromtimestamp(offsets["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using delay offset parameters generated on {datestring}") + logger.info(f"Using delay offset parameters generated on {datestring}") if len(offsets) > 0: # unpack dictionary @@ -311,14 +326,14 @@ def add_offsets( weights = [] preserve_mean = [] reductions = [] - if verbose: - print("Delay offset parameters:") + if not suppress_output: + logger.info("Delay offset parameters:") for k, v in offsets.items(): if k == "creation_date": continue if k == "constant": constant = v - if verbose: + if not suppress_output: print(f" Constant: {constant} ") elif k == "flip_delay_axis": fda = str(v) @@ -330,8 +345,8 @@ def add_offsets( raise ValueError( f"Invalid value for flip_delay_axis in config: {flip_delay_axis}.", ) - if verbose: - print(f" Flip delay axis: {flip_delay_axis} ") + if not suppress_output: + logger.info(f" Flip delay axis: {flip_delay_axis} ") else: columns.append(k) try: @@ -343,9 +358,9 @@ def add_offsets( preserve_mean.append(pm) red = v.get("reduction", None) reductions.append(red) - if verbose: - print( - f" Column[{k}]: Weight={weight}, Preserve Mean: {pm}, ", + if not suppress_output: + logger.info( + f" Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " f"Reductions: {red}.", ) diff --git a/sed/core/processor.py b/sed/core/processor.py index 29f52851..6af1cd06 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -6,6 +6,8 @@ import pathlib from collections.abc import Sequence from datetime import datetime +from logging import INFO +from logging import WARNING from typing import Any from typing import cast @@ -64,7 +66,7 @@ class SedProcessor: collect_metadata (bool): Option to collect metadata from files. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"] or False. + Defaults to config["core"]["verbose"] or True. **kwds: Keyword arguments passed to the reader. """ @@ -99,7 +101,7 @@ def __init__( collect_metadata (bool, optional): Option to collect metadata from files. Defaults to False. verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"] or False. + Defaults to config["core"]["verbose"] or True. **kwds: Keyword arguments passed to parse_config and to the reader. """ # split off config keywords @@ -113,11 +115,16 @@ def __init__( if num_cores >= N_CPU: num_cores = N_CPU - 1 self._config["core"]["num_cores"] = num_cores + logger.debug(f"Use {num_cores} cores for processing.") if verbose is None: - self.verbose = self._config["core"].get("verbose", False) + self.verbose = self._config["core"].get("verbose", True) else: self.verbose = verbose + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) self._dataframe: pd.DataFrame | ddf.DataFrame = None self._timed_dataframe: pd.DataFrame | ddf.DataFrame = None @@ -134,22 +141,28 @@ def __init__( self.loader = get_loader( loader_name=loader_name, config=self._config, + verbose=verbose, ) + logger.debug(f"Use loader: {loader_name}") self.ec = EnergyCalibrator( loader=get_loader( loader_name=loader_name, config=self._config, + verbose=verbose, ), config=self._config, + verbose=self.verbose, ) self.mc = MomentumCorrector( config=self._config, + verbose=self.verbose, ) self.dc = DelayCalibrator( config=self._config, + verbose=self.verbose, ) self.use_copy_tool = self._config.get("core", {}).get( @@ -164,6 +177,11 @@ def __init__( num_cores=self._config["core"]["num_cores"], **self._config["core"].get("copy_tool_kwds", {}), ) + logger.debug( + f"Initialized copy tool: Copy file from " + f"'{self._config['core']['copy_tool_source']}' " + f"to '{self._config['core']['copy_tool_dest']}'.", + ) except KeyError: self.use_copy_tool = False @@ -1737,7 +1755,6 @@ def calibrate_delay_axis( delay_range: tuple[float, float] = None, datafile: str = None, preview: bool = False, - verbose: bool = None, **kwds, ): """Append delay column to dataframe. Either provide delay ranges, or read @@ -1750,20 +1767,14 @@ def calibrate_delay_axis( Defaults to None. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``. """ - if verbose is None: - verbose = self.verbose - adc_column = self._config["dataframe"]["adc_column"] if adc_column not in self._dataframe.columns: raise ValueError(f"ADC column {adc_column} not found in dataframe, cannot calibrate!") if self._dataframe is not None: - if verbose: - logger.info("Adding delay column to dataframe:") + logger.info("Adding delay column to dataframe:") if delay_range is None and datafile is None: if len(self.dc.calibration) == 0: @@ -1778,7 +1789,6 @@ def calibrate_delay_axis( self._dataframe, delay_range=delay_range, datafile=datafile, - verbose=verbose, **kwds, ) if self._timed_dataframe is not None and adc_column in self._timed_dataframe.columns: @@ -1786,7 +1796,7 @@ def calibrate_delay_axis( self._timed_dataframe, delay_range=delay_range, datafile=datafile, - verbose=False, + suppress_output=True, **kwds, ) @@ -1804,8 +1814,7 @@ def calibrate_delay_axis( if preview: logger.info(self._dataframe.head(10)) else: - if self.verbose: - logger.info(self._dataframe) + logger.debug(self._dataframe) def save_delay_calibration( self, @@ -1898,7 +1907,6 @@ def add_delay_offset( weights=weights, reductions=reductions, preserve_mean=preserve_mean, - verbose=verbose, ) if self._timed_dataframe is not None and delay_column in self._timed_dataframe.columns: tdf, _ = self.dc.add_offsets( @@ -1910,7 +1918,7 @@ def add_delay_offset( weights=weights, reductions=reductions, preserve_mean=preserve_mean, - verbose=False, + suppress_output=True, ) self._attributes.add( From 329265852186c5aaeeb2f2a7d313c3a0cfe41e9d Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 13 Aug 2024 22:41:44 +0200 Subject: [PATCH 188/300] use logging in momentum calibrator --- sed/calibrator/momentum.py | 113 +++++++++++++++++++++---------------- sed/core/processor.py | 41 ++------------ 2 files changed, 71 insertions(+), 83 deletions(-) diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 44f1def2..572a08fa 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -6,6 +6,8 @@ import itertools as it from copy import deepcopy from datetime import datetime +from logging import INFO +from logging import WARNING from typing import Any import bokeh.palettes as bp @@ -32,6 +34,11 @@ from symmetrize import sym from symmetrize import tps +from sed.core.logging import setup_logging + +# Configure logging +logger = setup_logging("momentum") + class MomentumCorrector: """ @@ -44,6 +51,8 @@ class MomentumCorrector: provided as np.ndarray. Defaults to None. rotsym (int, optional): Rotational symmetry of the data. Defaults to 6. config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ def __init__( @@ -52,6 +61,7 @@ def __init__( bin_ranges: list[tuple] = None, rotsym: int = 6, config: dict = None, + verbose: bool = True, ): """Constructor of the MomentumCorrector class. @@ -62,12 +72,20 @@ def __init__( if provided as np.ndarray. Defaults to None. rotsym (int, optional): Rotational symmetry of the data. Defaults to 6. config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ if config is None: config = {} self._config = config + self.verbose = verbose + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) + self.image: np.ndarray = None self.img_ndim: int = None self.slice: np.ndarray = None @@ -308,6 +326,8 @@ def select_slice( if self.slice is not None: self.slice_corrected = self.slice_transformed = self.slice + logger.debug(f"Selected energy slice {selector} for momentum correction.") + elif self.img_ndim == 2: raise ValueError("Input image dimension is already 2!") @@ -594,7 +614,6 @@ def spline_warp_estimate( fixed_center: bool = True, interp_order: int = 1, ascale: float | list | tuple | np.ndarray = None, - verbose: bool = True, **kwds, ) -> np.ndarray: """Estimate the spline deformation field using thin plate spline registration. @@ -618,8 +637,6 @@ def spline_warp_estimate( points to be located along the principal axes (X/Y points of the Brillouin zone). Otherwise, an array with ``rotsym`` elements is expected, containing relative scales for each feature. Defaults to an array of equal scales. - verbose (bool, optional): Option to report the used landmarks for correction. - Defaults to True. **kwds: keyword arguments: - **landmarks**: (list/array): Landmark positions (row, column) used @@ -647,7 +664,6 @@ def spline_warp_estimate( if self.pouter is not None: self.pouter_ord = po.pointset_order(self.pouter) self.correction["creation_date"] = datetime.now().timestamp() - self.correction["creation_date"] = datetime.now().timestamp() else: try: features = np.asarray( @@ -661,22 +677,20 @@ def spline_warp_estimate( if ascale is not None: ascale = np.asarray(ascale) - if verbose: - if "creation_date" in self.correction: - datestring = datetime.fromtimestamp( - self.correction["creation_date"], - ).strftime( - "%m/%d/%Y, %H:%M:%S", - ) - print( - "No landmarks defined, using momentum correction parameters " - f"generated on {datestring}", - ) - else: - print( - "No landmarks defined, using momentum correction parameters " - "from config.", - ) + if "creation_date" in self.correction: + datestring = datetime.fromtimestamp( + self.correction["creation_date"], + ).strftime + ("%m/%d/%Y, %H:%M:%S",) + logger.info( + "No landmarks defined, using momentum correction parameters " + f"generated on {datestring}", + ) + else: + logger.info( + "No landmarks defined, using momentum correction parameters " + "from config.", + ) except KeyError as exc: raise ValueError( "No valid landmarks defined, and no landmarks found in configuration!", @@ -786,11 +800,13 @@ def spline_warp_estimate( if self.slice is not None: self.slice_corrected = corrected_image - if verbose: - print("Calculated thin spline correction based on the following landmarks:") - print(f"pouter: {self.pouter}") - if use_center: - print(f"pcent: {self.pcent}") + log_str = ( + "Calculated thin spline correction based on the following landmarks:\n" + f"pouter_ord: {self.pouter_ord}" + ) + if use_center: + log_str += f"\npcent: {self.pcent}" + logger.info(log_str) return corrected_image @@ -1042,7 +1058,6 @@ def pose_adjustment( transformations: dict[str, Any] = None, apply: bool = False, reset: bool = True, - verbose: bool = True, **kwds, ): """Interactive panel to adjust transformations that are applied to the image. @@ -1057,8 +1072,6 @@ def pose_adjustment( Defaults to False. reset (bool, optional): Option to reset the correction before transformation. Defaults to True. - verbose (bool, optional): - Option to report the performed transformations. Defaults to True. **kwds: Keyword parameters defining defaults for the transformations: - **scale** (float): Initial value of the scaling slider. @@ -1105,11 +1118,11 @@ def pose_adjustment( f"pose_adjustment() got unexpected keyword arguments {kwds.keys()}.", ) - elif "creation_date" in transformations and verbose: + elif "creation_date" in transformations: datestring = datetime.fromtimestamp(transformations["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using transformation parameters generated on {datestring}") + logger.info(f"Using transformation parameters generated on {datestring}") def update(scale: float, xtrans: float, ytrans: float, angle: float): transformed_image = source_image @@ -1196,9 +1209,7 @@ def apply_func(apply: bool): # noqa: ARG001 yscale=transformations["scale"], keep=True, ) - if verbose: - with results_box: - print(f"Applied scaling with scale={transformations['scale']}.") + logger.info(f"Applied scaling with scale={transformations['scale']}.") if transformations.get("xtrans", 0) != 0 or transformations.get("ytrans", 0) != 0: self.coordinate_transform( transform_type="translation", @@ -1206,12 +1217,10 @@ def apply_func(apply: bool): # noqa: ARG001 ytrans=transformations.get("ytrans", 0), keep=True, ) - if verbose: - with results_box: - print( - f"Applied translation with (xtrans={transformations.get('xtrans', 0)},", - f"ytrans={transformations.get('ytrans', 0)}).", - ) + logger.info( + f"Applied translation with (xtrans={transformations.get('xtrans', 0)}, " + f"ytrans={transformations.get('ytrans', 0)}).", + ) if transformations.get("angle", 0) != 0: self.coordinate_transform( transform_type="rotation", @@ -1219,9 +1228,7 @@ def apply_func(apply: bool): # noqa: ARG001 center=center, keep=True, ) - if verbose: - with results_box: - print(f"Applied rotation with angle={transformations['angle']}.") + logger.info(f"Applied rotation with angle={transformations['angle']}.") display(results_box) @@ -1237,10 +1244,10 @@ def apply_func(apply: bool): # noqa: ARG001 transformations["creation_date"] = datetime.now().timestamp() self.transformations = transformations - if verbose: + if self.verbose: plt.figure() subs = 20 - plt.title("Deformation field") + plt.title("Final Deformation field") plt.scatter( self.rdeform_field[::subs, ::subs].ravel(), self.cdeform_field[::subs, ::subs].ravel(), @@ -1659,6 +1666,12 @@ def calibrate( pixel_distance = norm(point_a - point_b) # Calculate the pixel to momentum conversion factor xratio = yratio = k_distance / pixel_distance + logger.debug( + f"Momentum calibration performed using the following parameters:\n" + f"point_a={point_a}\n" + f"point_b={point_b}\n" + f"k_distance={k_distance}", + ) else: assert k_coord_a is not None @@ -1669,6 +1682,13 @@ def calibrate( # Calculate the column- and row-wise conversion factor xratio = (kxa - kxb) / (point_a[0] - point_b[0]) yratio = (kya - kyb) / (point_a[1] - point_b[1]) + logger.debug( + f"Momentum calibration performed using the following parameters:\n" + f"point_a={point_a}\n" + f"point_b={point_b}\n" + f"k_coord_a={k_coord_a}\n" + f"k_coord_b={k_coord_b}", + ) k_row = rowdist * xratio + k_coord_b[0] k_col = coldist * yratio + k_coord_b[1] @@ -1705,7 +1725,6 @@ def apply_corrections( y_column: str = None, new_x_column: str = None, new_y_column: str = None, - verbose: bool = True, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and replace the X and Y values with their distortion-corrected version. @@ -1723,8 +1742,6 @@ def apply_corrections( new_y_column (str, optional): Label of the 'Y' column after momentum distortion correction. Defaults to config["momentum"]["corrected_y_column"]. - verbose (bool, optional): Option to report the used landmarks for correction. - Defaults to True. Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with @@ -1745,7 +1762,7 @@ def apply_corrections( if self.correction or self.transformations: if self.correction: # Generate spline warp from class features or config - self.spline_warp_estimate(verbose=verbose) + self.spline_warp_estimate() if self.transformations: # Apply config pose adjustments self.pose_adjustment() diff --git a/sed/core/processor.py b/sed/core/processor.py index 6af1cd06..0aeda86d 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -620,7 +620,6 @@ def define_features( def generate_splinewarp( self, use_center: bool = None, - verbose: bool = None, **kwds, ): """3. Step of the distortion correction workflow: Generate the correction @@ -629,16 +628,12 @@ def generate_splinewarp( Args: use_center (bool, optional): Option to use the position of the center point in the correction. Default is read from config, or set to True. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword arguments for MomentumCorrector.spline_warp_estimate(). """ - if verbose is None: - verbose = self.verbose - self.mc.spline_warp_estimate(use_center=use_center, verbose=verbose, **kwds) + self.mc.spline_warp_estimate(use_center=use_center, **kwds) - if self.mc.slice is not None and verbose: + if self.mc.slice is not None and self.verbose: print("Original slice with reference features") self.mc.view(annotated=True, backend="bokeh", crosshair=True) @@ -712,7 +707,6 @@ def pose_adjustment( apply: bool = False, use_correction: bool = True, reset: bool = True, - verbose: bool = None, **kwds, ): """3. step of the distortion correction workflow: Generate an interactive panel @@ -729,8 +723,6 @@ def pose_adjustment( or not. Defaults to True. reset (bool, optional): Option to reset the correction before transformation. Defaults to True. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword parameters defining defaults for the transformations: - **scale** (float): Initial value of the scaling slider. @@ -738,9 +730,6 @@ def pose_adjustment( - **ytrans** (float): Initial value of the ytrans slider. - **angle** (float): Initial value of the angle slider. """ - if verbose is None: - verbose = self.verbose - # Generate homography as default if no distortion correction has been applied if self.mc.slice_corrected is None: if self.mc.slice is None: @@ -752,13 +741,12 @@ def pose_adjustment( if self.mc.cdeform_field is None or self.mc.rdeform_field is None: # Generate distortion correction from config values - self.mc.spline_warp_estimate(verbose=verbose) + self.mc.spline_warp_estimate() self.mc.pose_adjustment( transformations=transformations, apply=apply, reset=reset, - verbose=verbose, **kwds, ) @@ -801,7 +789,6 @@ def save_transformations( def apply_momentum_correction( self, preview: bool = False, - verbose: bool = None, **kwds, ): """Applies the distortion correction and pose adjustment (optional) @@ -810,8 +797,6 @@ def apply_momentum_correction( Args: preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword parameters for ``MomentumCorrector.apply_correction``: - **rdeform_field** (np.ndarray, optional): Row deformation field. @@ -819,18 +804,13 @@ def apply_momentum_correction( - **inv_dfield** (np.ndarray, optional): Inverse deformation field. """ - if verbose is None: - verbose = self.verbose - x_column = self._config["dataframe"]["x_column"] y_column = self._config["dataframe"]["y_column"] if self._dataframe is not None: - if verbose: - logger.info("Adding corrected X/Y columns to dataframe:") + logger.info("Adding corrected X/Y columns to dataframe:") df, metadata = self.mc.apply_corrections( df=self._dataframe, - verbose=verbose, **kwds, ) if ( @@ -840,7 +820,6 @@ def apply_momentum_correction( ): tdf, _ = self.mc.apply_corrections( self._timed_dataframe, - verbose=False, **kwds, ) @@ -862,8 +841,7 @@ def apply_momentum_correction( if preview: logger.info(self._dataframe.head(10)) else: - if self.verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) # Momentum calibration work flow # 1. Calculate momentum calibration @@ -957,7 +935,6 @@ def apply_momentum_calibration( self, calibration: dict = None, preview: bool = False, - verbose: bool = None, **kwds, ): """2. step of the momentum calibration work flow: Apply the momentum @@ -969,13 +946,8 @@ def apply_momentum_calibration( use. Defaults to None. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword args passed to ``MomentumCalibrator.append_k_axis``. """ - if verbose is None: - verbose = self.verbose - x_column = self._config["dataframe"]["x_column"] y_column = self._config["dataframe"]["y_column"] @@ -1015,8 +987,7 @@ def apply_momentum_calibration( if preview: logger.info(self._dataframe.head(10)) else: - if self.verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) # Energy correction workflow # 1. Adjust the energy correction parameters From e3af246803fea24c46cf0e0b608789ee2fadce27 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 14 Aug 2024 00:15:10 +0200 Subject: [PATCH 189/300] use logging in energy calibrator, and further fixes --- sed/calibrator/delay.py | 21 ++-- sed/calibrator/energy.py | 118 +++++++++++------- sed/core/processor.py | 115 ++++++----------- sed/loader/mpes/loader.py | 11 -- ...for_example_time-resolved_ARPES_data.ipynb | 1 - 5 files changed, 121 insertions(+), 145 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index d259bb60..c4d8f797 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -326,15 +326,13 @@ def add_offsets( weights = [] preserve_mean = [] reductions = [] - if not suppress_output: - logger.info("Delay offset parameters:") + log_str = "Delay offset parameters:" for k, v in offsets.items(): if k == "creation_date": continue if k == "constant": constant = v - if not suppress_output: - print(f" Constant: {constant} ") + log_str += f"\n Constant: {constant}" elif k == "flip_delay_axis": fda = str(v) if fda.lower() in ["true", "1"]: @@ -345,8 +343,7 @@ def add_offsets( raise ValueError( f"Invalid value for flip_delay_axis in config: {flip_delay_axis}.", ) - if not suppress_output: - logger.info(f" Flip delay axis: {flip_delay_axis} ") + log_str += f"\n Flip delay axis: {flip_delay_axis}" else: columns.append(k) try: @@ -358,11 +355,13 @@ def add_offsets( preserve_mean.append(pm) red = v.get("reduction", None) reductions.append(red) - if not suppress_output: - logger.info( - f" Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " - f"Reductions: {red}.", - ) + log_str += ( + f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"Reductions: {red}." + ) + + if not suppress_output: + logger.info(log_str) if len(columns) > 0: df = dfops.offset_by_other_columns( diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 62e5a4ff..ca7c2f10 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -8,6 +8,8 @@ from copy import deepcopy from datetime import datetime from functools import partial +from logging import INFO +from logging import WARNING from typing import Any from typing import cast from typing import Literal @@ -28,15 +30,19 @@ from IPython.display import display from lmfit import Minimizer from lmfit import Parameters -from lmfit.printfuncs import report_fit +from lmfit.printfuncs import fit_report from numpy.linalg import lstsq from scipy.signal import savgol_filter from scipy.sparse.linalg import lsqr from sed.binning import bin_dataframe from sed.core import dfops +from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader +# Configure logging +logger = setup_logging("delay") + class EnergyCalibrator: """Electron binding energy calibration workflow. @@ -53,6 +59,8 @@ class EnergyCalibrator: tof (np.ndarray, optional): TOF-values for the data traces. Defaults to None. config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ def __init__( @@ -62,6 +70,7 @@ def __init__( traces: np.ndarray = None, tof: np.ndarray = None, config: dict = None, + verbose: bool = True, ): """For the initialization of the EnergyCalibrator class an instance of a loader is required. The data can be loaded using the optional arguments, @@ -75,7 +84,20 @@ def __init__( tof (np.ndarray, optional): TOF-values for the data traces. Defaults to None. config (dict, optional): Config dictionary. Defaults to None. + verbose (bool, optional): Option to print out diagnostic information. + Defaults to True. """ + if config is None: + config = {} + + self._config = config + + self.verbose = verbose + if self.verbose: + logger.handlers[0].setLevel(INFO) + else: + logger.handlers[0].setLevel(WARNING) + self.loader = loader self.biases: np.ndarray = None self.traces: np.ndarray = None @@ -85,11 +107,6 @@ def __init__( if traces is not None and tof is not None and biases is not None: self.load_data(biases=biases, traces=traces, tof=tof) - if config is None: - config = {} - - self._config = config - self.featranges: list[tuple] = [] # Value ranges for feature detection self.peaks: np.ndarray = np.asarray([]) self.calibration: dict[str, Any] = self._config["energy"].get("calibration", {}) @@ -276,6 +293,7 @@ def normalize(self, smooth: bool = False, span: int = 7, order: int = 1): span=span, order=order, ) + logger.debug("Normalized energy calibration traces.") def adjust_ranges( self, @@ -393,7 +411,9 @@ def apply_func(apply: bool): # noqa: ARG001 traces=self.traces_normed, **kwds, ) + logger.info(f"Use feature ranges: {self.featranges}.") self.feature_extract(peak_window=7) + logger.info(f"Extracted energy features: {self.peaks}.") ranges_slider.close() refid_slider.close() apply_button.close() @@ -499,7 +519,6 @@ def calibrate( landmarks: np.ndarray = None, biases: np.ndarray = None, t: np.ndarray = None, - verbose: bool = True, **kwds, ) -> dict: """Calculate the functional mapping between time-of-flight and the energy @@ -523,8 +542,6 @@ def calibrate( calibration. Defaults to self.peaks. biases (np.ndarray, optional): Bias values. Defaults to self.biases. t (np.ndarray, optional): TOF values. Defaults to self.tof. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. **kwds: keyword arguments. See available keywords for ``poly_energy_calibration()`` and ``fit_energy_calibration()`` @@ -564,7 +581,6 @@ def calibrate( ref_energy=ref_energy, t=t, energy_scale=energy_scale, - verbose=verbose, **kwds, ) elif method in ("lstsq", "lsqr"): @@ -584,7 +600,7 @@ def calibrate( self.calibration["creation_date"] = datetime.now().timestamp() return self.calibration - def view( # pylint: disable=dangerous-default-value + def view( self, traces: np.ndarray, segs: list[tuple] = None, @@ -634,7 +650,7 @@ def view( # pylint: disable=dangerous-default-value sign = 1 if energy_scale == "kinetic" else -1 if backend == "matplotlib": - figsize = kwds.pop("figsize", (12, 4)) + figsize = kwds.pop("figsize", (6, 4)) fig_plt, ax = plt.subplots(figsize=figsize) for itr, trace in enumerate(traces): if align: @@ -762,7 +778,7 @@ def append_energy_axis( energy_column: str = None, calibration: dict = None, bias_voltage: float = None, - verbose: bool = True, + suppress_output: bool = False, **kwds, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the energy axis to the events dataframe. @@ -780,8 +796,8 @@ def append_energy_axis( bias_voltage (float, optional): Sample bias voltage of the scan data. If omitted, the bias voltage is being read from the dataframe. If it is not found there, a warning is printed and the calibrated data might have an offset. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. + verbose (bool, optional): Option to suppress output of diagnostic information. + Defaults to False. **kwds: additional keyword arguments for the energy conversion. They are added to the calibration dictionary. @@ -814,11 +830,11 @@ def append_energy_axis( calibration[key] = value calibration["creation_date"] = datetime.now().timestamp() - elif "creation_date" in calibration and verbose: + elif "creation_date" in calibration and not suppress_output: datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using energy calibration parameters generated on {datestring}") + logger.info(f"Using energy calibration parameters generated on {datestring}") # try to determine calibration type if not provided if "calib_type" not in calibration: @@ -870,10 +886,20 @@ def append_energy_axis( else: raise NotImplementedError + if not suppress_output: + report_dict = { + "calib_type": calibration["calib_type"], + "fit_function": calibration["fit_function"], + "coefficients": calibration["coefficients"], + } + logger.debug(f"Used energy calibration parameters: {report_dict}.") + # apply bias offset scale_sign: Literal[-1, 1] = -1 if calibration["energy_scale"] == "binding" else 1 if bias_voltage is not None: df[energy_column] = df[energy_column] + scale_sign * bias_voltage + if not suppress_output: + logger.debug(f"Shifted energy column by constant bias value: {bias_voltage}.") elif self._config["dataframe"]["bias_column"] in df.columns: df = dfops.offset_by_other_columns( df=df, @@ -881,8 +907,15 @@ def append_energy_axis( offset_columns=self._config["dataframe"]["bias_column"], weights=scale_sign, ) + if not suppress_output: + logger.debug( + "Shifted energy column by bias column: " + f"{self._config['dataframe']['bias_column']}.", + ) else: - print("Sample bias data not found or provided. Calibrated energy might be incorrect.") + logger.warning( + "Sample bias data not found or provided. Calibrated energy might be incorrect.", + ) metadata = self.gather_calibration_metadata(calibration) @@ -1324,7 +1357,7 @@ def apply_energy_correction( correction_type: str = None, amplitude: float = None, correction: dict = None, - verbose: bool = True, + suppress_output: bool = False, **kwds, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Apply correction to the time-of-flight (TOF) axis of single-event data. @@ -1350,8 +1383,8 @@ def apply_energy_correction( correction (dict, optional): Correction dictionary containing parameters for the correction. Defaults to self.correction or config["energy"]["correction"]. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. + suppress_output (bool, optional): Option to suppress output of diagnostic information. + Defaults to False. **kwds: Additional parameters to use for the correction: - **x_column** (str): Name of the x column. @@ -1394,16 +1427,19 @@ def apply_energy_correction( correction["creation_date"] = datetime.now().timestamp() - elif "creation_date" in correction and verbose: + elif "creation_date" in correction and not suppress_output: datestring = datetime.fromtimestamp(correction["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using energy correction parameters generated on {datestring}") + logger.info(f"Using energy correction parameters generated on {datestring}") missing_keys = {"correction_type", "center", "amplitude"} - set(correction.keys()) if missing_keys: raise ValueError(f"Required correction parameters '{missing_keys}' missing!") + if not suppress_output: + logger.debug(f"Correction parameters:\n{correction}") + df[new_tof_column] = df[tof_column] + correction_function( x=df[x_column], y=df[y_column], @@ -1489,7 +1525,7 @@ def add_offsets( preserve_mean: bool | Sequence[bool] = False, reductions: str | Sequence[str] = None, energy_column: str = None, - verbose: bool = True, + suppress_output: bool = False, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Apply an offset to the energy column by the values of the provided columns. @@ -1512,8 +1548,8 @@ def add_offsets( If None, the shift is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported. energy_column (str, optional): Name of the column containing the energy values. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. + suppress_output (bool, optional): Option to suppress output of diagnostic information. + Defaults to False. Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with the new columns @@ -1584,11 +1620,11 @@ def add_offsets( elif constant is not None: raise TypeError(f"Invalid type for constant: {type(constant)}") - elif "creation_date" in offsets and verbose: + elif "creation_date" in offsets and not suppress_output: datestring = datetime.fromtimestamp(offsets["creation_date"]).strftime( "%m/%d/%Y, %H:%M:%S", ) - print(f"Using energy offset parameters generated on {datestring}") + logger.info(f"Using energy offset parameters generated on {datestring}") if len(offsets) > 0: # unpack dictionary @@ -1597,16 +1633,14 @@ def add_offsets( weights = [] preserve_mean = [] reductions = [] - if verbose: - print("Energy offset parameters:") + log_str = "Energy offset parameters:" for k, v in offsets.items(): if k == "creation_date": continue if k == "constant": # flip sign if binding energy scale constant = v * scale_sign - if verbose: - print(f" Constant: {constant} ") + log_str += f"\n Constant: {constant}" else: columns.append(k) try: @@ -1628,11 +1662,13 @@ def add_offsets( if str(red).lower() in ["none", "null"]: red = None reductions.append(red) - if verbose: - print( - f" Column[{k}]: Weight={weight}, Preserve Mean: {pm}, ", - f"Reductions: {red}.", - ) + log_str += ( + f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"Reductions: {red}." + ) + + if not suppress_output: + logger.info(log_str) if len(columns) > 0: df = dfops.offset_by_other_columns( @@ -1940,7 +1976,7 @@ def peaksearch( try: pkmaxs.append(maxs[0, :]) except IndexError: # No peak found for this range - print(f"No peak detected in range {rng}.") + logger.error(f"No peak detected in range {rng}.") raise if plot: @@ -2111,7 +2147,6 @@ def fit_energy_calibration( ref_energy: float, t: list[float] | np.ndarray = None, energy_scale: str = "kinetic", - verbose: bool = True, **kwds, ) -> dict: """Energy calibration by nonlinear least squares fitting of spectral landmarks on @@ -2133,8 +2168,6 @@ def fit_energy_calibration( - **'kinetic'**: increasing energy with decreasing TOF. - **'binding'**: increasing energy with increasing TOF. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to True. **kwds: keyword arguments: - **t0** (float): constrains and initial values for the fit parameter t0, @@ -2201,8 +2234,7 @@ def residual(pars, time, data, binwidth, binning, energy_scale): fcn_args=(pos, vals, binwidth, binning, energy_scale), ) result = fit.leastsq() - if verbose: - report_fit(result) + logger.info(fit_report(result)) # Construct the calibrating function pfunc = partial( diff --git a/sed/core/processor.py b/sed/core/processor.py index 0aeda86d..47a8f204 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1075,7 +1075,6 @@ def apply_energy_correction( self, correction: dict = None, preview: bool = False, - verbose: bool = None, **kwds, ): """2. step of the energy correction workflow: Apply the energy correction @@ -1086,30 +1085,23 @@ def apply_energy_correction( parameters. Defaults to config["energy"]["calibration"]. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword args passed to ``EnergyCalibrator.apply_energy_correction()``. """ - if verbose is None: - verbose = self.verbose - tof_column = self._config["dataframe"]["tof_column"] if self._dataframe is not None: - if verbose: - logger.info("Applying energy correction to dataframe...") + logger.info("Applying energy correction to dataframe...") df, metadata = self.ec.apply_energy_correction( df=self._dataframe, correction=correction, - verbose=verbose, **kwds, ) if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns: tdf, _ = self.ec.apply_energy_correction( df=self._timed_dataframe, correction=correction, - verbose=False, + suppress_output=True, **kwds, ) @@ -1126,8 +1118,7 @@ def apply_energy_correction( if preview: logger.info(self._dataframe.head(10)) else: - if verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) # Energy calibrator workflow # 1. Load and normalize data @@ -1195,6 +1186,7 @@ def load_bias_series( "If binned_data is provided as tuple, it needs to contain " "(tof, biases, traces)!", ) from exc + logger.debug(f"Energy calibration data loaded from binned data. Bias values={biases}.") self.ec.load_data(biases=biases, traces=traces, tof=tof) elif data_files is not None: @@ -1206,6 +1198,10 @@ def load_bias_series( biases=biases, bias_key=bias_key, ) + logger.debug( + f"Energy calibration data binned from files {data_files} data. " + f"Bias values={biases}.", + ) else: raise ValueError("Either binned_data or data_files needs to be provided!") @@ -1272,9 +1268,10 @@ def find_bias_peaks( mode=mode, radius=radius, ) - logger.info(self.ec.featranges) + logger.info(f"Use feature ranges: {self.ec.featranges}.") try: self.ec.feature_extract(peak_window=peak_window) + logger.info(f"Extracted energy features: {self.ec.peaks}.") self.ec.view( traces=self.ec.traces_normed, segs=self.ec.featranges, @@ -1283,7 +1280,7 @@ def find_bias_peaks( backend="bokeh", ) except IndexError: - print("Could not determine all peaks!") + logger.error("Could not determine all peaks!") raise else: # New adjustment tool @@ -1305,7 +1302,6 @@ def calibrate_energy_axis( ref_energy: float, method: str = None, energy_scale: str = None, - verbose: bool = None, **kwds, ): """3. Step of the energy calibration workflow: Calculate the calibration @@ -1327,13 +1323,8 @@ def calibrate_energy_axis( - **'binding'**: increasing energy with increasing TOF. Defaults to config["energy"]["energy_scale"] - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds**: Keyword parameters passed to ``EnergyCalibrator.calibrate()``. """ - if verbose is None: - verbose = self.verbose - if method is None: method = self._config["energy"]["calibration_method"] @@ -1344,25 +1335,28 @@ def calibrate_energy_axis( ref_energy=ref_energy, method=method, energy_scale=energy_scale, - verbose=verbose, **kwds, ) - if verbose: - print("Quality of Calibration:") + if self.verbose: self.ec.view( traces=self.ec.traces_normed, xaxis=self.ec.calibration["axis"], align=True, energy_scale=energy_scale, - backend="bokeh", + backend="matplotlib", + title="Quality of Calibration", ) - print("E/TOF relationship:") + plt.xlabel("Energy (eV)") + plt.ylabel("Intensity") + plt.tight_layout() + plt.show() if energy_scale == "kinetic": self.ec.view( traces=self.ec.calibration["axis"][None, :] + self.ec.biases[0], xaxis=self.ec.tof, backend="matplotlib", show_legend=False, + title="E/TOF relationship", ) plt.scatter( self.ec.peaks[:, 0], @@ -1370,12 +1364,14 @@ def calibrate_energy_axis( s=50, c="k", ) + plt.tight_layout() elif energy_scale == "binding": self.ec.view( traces=self.ec.calibration["axis"][None, :] - self.ec.biases[0], xaxis=self.ec.tof, backend="matplotlib", show_legend=False, + title="E/TOF relationship", ) plt.scatter( self.ec.peaks[:, 0], @@ -1388,8 +1384,9 @@ def calibrate_energy_axis( 'energy_scale needs to be either "binding" or "kinetic"', f", got {energy_scale}.", ) - plt.xlabel("Time-of-flight", fontsize=15) - plt.ylabel("Energy (eV)", fontsize=15) + plt.xlabel("Time-of-flight") + plt.ylabel("Energy (eV)") + plt.tight_layout() plt.show() # 3a. Save energy calibration parameters to config file. @@ -1435,7 +1432,6 @@ def append_energy_axis( calibration: dict = None, bias_voltage: float = None, preview: bool = False, - verbose: bool = None, **kwds, ): """4. step of the energy calibration workflow: Apply the calibration function @@ -1451,24 +1447,17 @@ def append_energy_axis( the bias voltage is being read from the dataframe. If it is not found there, a warning is printed and the calibrated data might have an offset. preview (bool): Option to preview the first elements of the data frame. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: Keyword args passed to ``EnergyCalibrator.append_energy_axis()``. """ - if verbose is None: - verbose = self.verbose - tof_column = self._config["dataframe"]["tof_column"] if self._dataframe is not None: - if verbose: - logger.info("Adding energy column to dataframe:") + logger.info("Adding energy column to dataframe:") df, metadata = self.ec.append_energy_axis( df=self._dataframe, calibration=calibration, bias_voltage=bias_voltage, - verbose=verbose, **kwds, ) if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns: @@ -1476,7 +1465,7 @@ def append_energy_axis( df=self._timed_dataframe, calibration=calibration, bias_voltage=bias_voltage, - verbose=False, + suppress_output=True, **kwds, ) @@ -1495,8 +1484,7 @@ def append_energy_axis( if preview: logger.info(self._dataframe.head(10)) else: - if verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) @call_logger(logger) def add_energy_offset( @@ -1507,7 +1495,6 @@ def add_energy_offset( reductions: str | Sequence[str] = None, preserve_mean: bool | Sequence[bool] = None, preview: bool = False, - verbose: bool = None, ) -> None: """Shift the energy axis of the dataframe by a given amount. @@ -1525,15 +1512,10 @@ def add_energy_offset( column before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. Raises: ValueError: If the energy column is not in the dataframe. """ - if verbose is None: - verbose = self.verbose - energy_column = self._config["dataframe"]["energy_column"] if energy_column not in self._dataframe.columns: raise ValueError( @@ -1541,8 +1523,7 @@ def add_energy_offset( "Run `append_energy_axis()` first.", ) if self.dataframe is not None: - if verbose: - logger.info("Adding energy offset to dataframe:") + logger.info("Adding energy offset to dataframe:") df, metadata = self.ec.add_offsets( df=self._dataframe, constant=constant, @@ -1551,7 +1532,6 @@ def add_energy_offset( weights=weights, reductions=reductions, preserve_mean=preserve_mean, - verbose=verbose, ) if self._timed_dataframe is not None and energy_column in self._timed_dataframe.columns: tdf, _ = self.ec.add_offsets( @@ -1562,6 +1542,7 @@ def add_energy_offset( weights=weights, reductions=reductions, preserve_mean=preserve_mean, + suppress_output=True, ) self._attributes.add( @@ -1578,7 +1559,7 @@ def add_energy_offset( raise ValueError("No dataframe loaded!") if preview: logger.info(self._dataframe.head(10)) - elif verbose: + else: logger.info(self._dataframe) def save_energy_offset( @@ -1610,7 +1591,6 @@ def save_energy_offset( def append_tof_ns_axis( self, preview: bool = False, - verbose: bool = None, **kwds, ): """Convert time-of-flight channel steps to nanoseconds. @@ -1621,19 +1601,13 @@ def append_tof_ns_axis( Defaults to config["dataframe"]["tof_ns_column"]. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: additional arguments are passed to ``EnergyCalibrator.append_tof_ns_axis()``. """ - if verbose is None: - verbose = self.verbose - tof_column = self._config["dataframe"]["tof_column"] if self._dataframe is not None: - if verbose: - logger.info("Adding time-of-flight column in nanoseconds to dataframe.") + logger.info("Adding time-of-flight column in nanoseconds to dataframe.") # TODO assert order of execution through metadata df, metadata = self.ec.append_tof_ns_axis( @@ -1659,15 +1633,13 @@ def append_tof_ns_axis( if preview: logger.info(self._dataframe.head(10)) else: - if verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) @call_logger(logger) def align_dld_sectors( self, sector_delays: np.ndarray = None, preview: bool = False, - verbose: bool = None, **kwds, ): """Align the 8s sectors of the HEXTOF endstation. @@ -1677,18 +1649,12 @@ def align_dld_sectors( config["dataframe"]["sector_delays"]. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. **kwds: additional arguments are passed to ``EnergyCalibrator.align_dld_sectors()``. """ - if verbose is None: - verbose = self.verbose - tof_column = self._config["dataframe"]["tof_column"] if self._dataframe is not None: - if verbose: - logger.info("Aligning 8s sectors of dataframe") + logger.info("Aligning 8s sectors of dataframe") # TODO assert order of execution through metadata df, metadata = self.ec.align_dld_sectors( @@ -1716,8 +1682,7 @@ def align_dld_sectors( if preview: logger.info(self._dataframe.head(10)) else: - if verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) # Delay calibration function @call_logger(logger) @@ -1834,7 +1799,6 @@ def add_delay_offset( reductions: str | Sequence[str] = None, preserve_mean: bool | Sequence[bool] = False, preview: bool = False, - verbose: bool = None, ) -> None: """Shift the delay axis of the dataframe by a constant or other columns. @@ -1853,22 +1817,16 @@ def add_delay_offset( column before applying the shift. Defaults to False. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. - verbose (bool, optional): Option to print out diagnostic information. - Defaults to config["core"]["verbose"]. Raises: ValueError: If the delay column is not in the dataframe. """ - if verbose is None: - verbose = self.verbose - delay_column = self._config["dataframe"]["delay_column"] if delay_column not in self._dataframe.columns: raise ValueError(f"Delay column {delay_column} not found in dataframe! ") if self.dataframe is not None: - if verbose: - logger.info("Adding delay offset to dataframe:") + logger.info("Adding delay offset to dataframe:") df, metadata = self.dc.add_offsets( df=self._dataframe, constant=constant, @@ -1905,8 +1863,7 @@ def add_delay_offset( if preview: logger.info(self._dataframe.head(10)) else: - if verbose: - logger.info(self._dataframe) + logger.info(self._dataframe) def save_delay_offsets( self, diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 22df8a0a..df4d0323 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -191,11 +191,6 @@ def hdf5_to_timed_dataframe( if channel["dataset_key"] in test_proc: electron_channels.append(channel) column_names.append(name) - else: - logger.warning( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " - "Skipping the channel.", - ) elif channel["format"] != "per_file": error_msg = f"Invalid 'format':{channel['format']} for channel {name}." logger.error(error_msg) @@ -246,12 +241,6 @@ def hdf5_to_timed_dataframe( ] dataframe = ddf.from_delayed(delayeds) - else: - logger.warning( - f"Entry \"{channel['dataset_key']}\" for channel \"{name}\" not found. " - "Skipping the channel.", - ) - return dataframe diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 34c448e1..82798d55 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -481,7 +481,6 @@ " d={'value':1.0,'min': .7, 'max':1.2, 'vary':True},\n", " t0={'value':8e-7, 'min': 1e-7, 'max': 1e-6, 'vary':True},\n", " E0={'value': 0., 'min': -100, 'max': 0, 'vary': True},\n", - " verbose=True,\n", ")" ] }, From 2a69836b962bb84eacca8444e4f44ba40fb4eeba Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 14 Aug 2024 00:47:05 +0200 Subject: [PATCH 190/300] some further fixes --- sed/calibrator/momentum.py | 14 ++++++++++++-- sed/core/processor.py | 1 + tutorial/4_hextof_workflow.ipynb | 1 - 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index 572a08fa..bae3bf65 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -680,8 +680,9 @@ def spline_warp_estimate( if "creation_date" in self.correction: datestring = datetime.fromtimestamp( self.correction["creation_date"], - ).strftime - ("%m/%d/%Y, %H:%M:%S",) + ).strftime( + "%m/%d/%Y, %H:%M:%S", + ) logger.info( "No landmarks defined, using momentum correction parameters " f"generated on {datestring}", @@ -1873,6 +1874,7 @@ def append_k_axis( new_x_column: str = None, new_y_column: str = None, calibration: dict = None, + suppress_output: bool = False, **kwds, ) -> tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: """Calculate and append the k axis coordinates (kx, ky) to the events dataframe. @@ -1892,6 +1894,8 @@ def append_k_axis( momentum calibration. Defaults to config["momentum"]["ky_column"]. calibration (dict, optional): Dictionary containing calibration parameters. Defaults to 'self.calibration' or config["momentum"]["calibration"]. + suppress_output (bool, optional): Option to suppress output of diagnostic information. + Defaults to False. **kwds: Keyword parameters for momentum calibration. Parameters are added to the calibration dictionary. @@ -1938,6 +1942,12 @@ def append_k_axis( if len(kwds) > 0: raise TypeError(f"append_k_axis() got unexpected keyword arguments {kwds.keys()}.") + if "creation_date" in calibration and not suppress_output: + datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( + "%m/%d/%Y, %H:%M:%S", + ) + logger.info(f"Using momentum calibration parameters generated on {datestring}") + try: (df[new_x_column], df[new_y_column]) = detector_coordinates_2_k_coordinates( r_det=df[x_column], diff --git a/sed/core/processor.py b/sed/core/processor.py index 47a8f204..247e13f4 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -966,6 +966,7 @@ def apply_momentum_calibration( tdf, _ = self.mc.append_k_axis( df=self._timed_dataframe, calibration=calibration, + suppress_output=True, **kwds, ) diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 743a7e15..c9b36c04 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -503,7 +503,6 @@ " d={'value':1.0,'min': .2, 'max':1.0, 'vary':False},\n", " t0={'value':5e-7, 'min': 1e-7, 'max': 1e-6, 'vary':True},\n", " E0={'value': 0., 'min': -100, 'max': 100, 'vary': True},\n", - " verbose=True,\n", ")" ] }, From 79a181d5551eb20f77b4f7e5a0cdecf0d0f1eaaf Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 19 Sep 2024 22:45:33 +0200 Subject: [PATCH 191/300] add function to set verbosity, and use in each class --- sed/calibrator/delay.py | 8 ++------ sed/calibrator/energy.py | 8 ++------ sed/calibrator/momentum.py | 8 ++------ sed/core/logging.py | 17 ++++++++++++++++- sed/core/processor.py | 8 ++------ sed/loader/flash/loader.py | 8 ++------ sed/loader/mpes/loader.py | 8 ++------ sed/loader/sxp/loader.py | 8 ++------ 8 files changed, 30 insertions(+), 43 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index c4d8f797..d465a462 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -5,8 +5,6 @@ from collections.abc import Sequence from copy import deepcopy from datetime import datetime -from logging import INFO -from logging import WARNING from typing import Any import dask.dataframe @@ -15,6 +13,7 @@ import pandas as pd from sed.core import dfops +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging # Configure logging @@ -50,10 +49,7 @@ def __init__( self._config = {} self.verbose = verbose - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.adc_column: str = self._config["dataframe"].get("adc_column", None) self.delay_column: str = self._config["dataframe"]["delay_column"] diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index ca7c2f10..1e50d745 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -8,8 +8,6 @@ from copy import deepcopy from datetime import datetime from functools import partial -from logging import INFO -from logging import WARNING from typing import Any from typing import cast from typing import Literal @@ -37,6 +35,7 @@ from sed.binning import bin_dataframe from sed.core import dfops +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader @@ -93,10 +92,7 @@ def __init__( self._config = config self.verbose = verbose - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.loader = loader self.biases: np.ndarray = None diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index bae3bf65..ac26f0e7 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -6,8 +6,6 @@ import itertools as it from copy import deepcopy from datetime import datetime -from logging import INFO -from logging import WARNING from typing import Any import bokeh.palettes as bp @@ -34,6 +32,7 @@ from symmetrize import sym from symmetrize import tps +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging # Configure logging @@ -81,10 +80,7 @@ def __init__( self._config = config self.verbose = verbose - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.image: np.ndarray = None self.img_ndim: int = None diff --git a/sed/core/logging.py b/sed/core/logging.py index 539f29b6..ad019dc8 100644 --- a/sed/core/logging.py +++ b/sed/core/logging.py @@ -29,7 +29,8 @@ def setup_logging( Args: name (str): The name of the logger. - base_name (str, optional): The name of the base logger. Defaults to "sed". + set_base_handler (bool, optional): Option to re-initialize the base handler logging to the + logfile. Defaults to False. user_log_path (str, optional): Path to the user-specific log directory. Defaults to DEFAULT_LOG_DIR. @@ -88,6 +89,20 @@ def setup_logging( return logger +def set_verbosity(logger: logging.Logger, verbose: bool) -> None: + """Sets log level for the given logger's default handler. + + Args: + logger (logging.Logger): The logger on which to set the log level. + verbose (bool): Sets loglevel to INFO if True, to WARNING otherwise. + """ + handler = logger.handlers[0] + if verbose: + handler.setLevel(logging.INFO) + else: + handler.setLevel(logging.WARNING) + + def call_logger(logger: logging.Logger): def log_call(func: Callable): @wraps(func) diff --git a/sed/core/processor.py b/sed/core/processor.py index 247e13f4..ea3ac37e 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -6,8 +6,6 @@ import pathlib from collections.abc import Sequence from datetime import datetime -from logging import INFO -from logging import WARNING from typing import Any from typing import cast @@ -30,6 +28,7 @@ from sed.core.dfops import apply_filter from sed.core.dfops import apply_jitter from sed.core.logging import call_logger +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging from sed.core.metadata import MetaHandler from sed.diagnostics import grid_histogram @@ -121,10 +120,7 @@ def __init__( self.verbose = self._config["core"].get("verbose", True) else: self.verbose = verbose - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self._dataframe: pd.DataFrame | ddf.DataFrame = None self._timed_dataframe: pd.DataFrame | ddf.DataFrame = None diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index b44b3b82..b17621fd 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -12,13 +12,12 @@ import re import time from collections.abc import Sequence -from logging import INFO -from logging import WARNING from pathlib import Path import dask.dataframe as dd from natsort import natsorted +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader from sed.loader.flash.buffer_handler import BufferHandler @@ -55,10 +54,7 @@ def __init__(self, config: dict, verbose: bool = True) -> None: """ super().__init__(config=config, verbose=verbose) - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof self.raw_dir: str = None diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index df4d0323..c130308a 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -10,8 +10,6 @@ import json import os from collections.abc import Sequence -from logging import INFO -from logging import WARNING from typing import Any from urllib.error import HTTPError from urllib.error import URLError @@ -25,6 +23,7 @@ import scipy.interpolate as sint from natsort import natsorted +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader @@ -571,10 +570,7 @@ def __init__( ): super().__init__(config=config, verbose=verbose) - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.read_timestamps = self._config.get("dataframe", {}).get( "read_timestamps", diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index e18a64f9..1c6d625e 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -14,8 +14,6 @@ import time from collections.abc import Sequence from functools import reduce -from logging import INFO -from logging import WARNING from pathlib import Path import dask.dataframe as dd @@ -30,6 +28,7 @@ from pandas import Series from sed.core import dfops +from sed.core.logging import set_verbosity from sed.core.logging import setup_logging from sed.loader.base.loader import BaseLoader from sed.loader.utils import parse_h5_keys @@ -57,10 +56,7 @@ class SXPLoader(BaseLoader): def __init__(self, config: dict, verbose: bool = True) -> None: super().__init__(config=config, verbose=verbose) - if self.verbose: - logger.handlers[0].setLevel(INFO) - else: - logger.handlers[0].setLevel(WARNING) + set_verbosity(logger, self.verbose) self.multi_index = ["trainId", "pulseId", "electronId"] self.index_per_electron: MultiIndex = None From 8716dc06c8b1efbe61fc24181d0fc70344c055fc Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 19 Sep 2024 23:00:18 +0200 Subject: [PATCH 192/300] Make verbose a private property, and add getter and setters for it, to propagate verbosity --- sed/calibrator/delay.py | 23 ++++++++++++++++++++-- sed/calibrator/energy.py | 23 ++++++++++++++++++++-- sed/calibrator/momentum.py | 25 +++++++++++++++++++++--- sed/core/processor.py | 39 ++++++++++++++++++++++++++++++-------- sed/loader/base/loader.py | 20 ++++++++++++++++++- sed/loader/flash/loader.py | 21 +++++++++++++++++++- sed/loader/mpes/loader.py | 21 +++++++++++++++++++- sed/loader/sxp/loader.py | 21 +++++++++++++++++++- 8 files changed, 174 insertions(+), 19 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index d465a462..000fe9cf 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -48,8 +48,8 @@ def __init__( else: self._config = {} - self.verbose = verbose - set_verbosity(logger, self.verbose) + self._verbose = verbose + set_verbosity(logger, self._verbose) self.adc_column: str = self._config["dataframe"].get("adc_column", None) self.delay_column: str = self._config["dataframe"]["delay_column"] @@ -60,6 +60,25 @@ def __init__( self.calibration: dict[str, Any] = self._config["delay"].get("calibration", {}) self.offsets: dict[str, Any] = self._config["delay"].get("offsets", {}) + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + def append_delay_axis( self, df: pd.DataFrame | dask.dataframe.DataFrame, diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 1e50d745..e3e7d1ca 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -91,8 +91,8 @@ def __init__( self._config = config - self.verbose = verbose - set_verbosity(logger, self.verbose) + self._verbose = verbose + set_verbosity(logger, self._verbose) self.loader = loader self.biases: np.ndarray = None @@ -125,6 +125,25 @@ def __init__( self.offsets: dict[str, Any] = self._config["energy"].get("offsets", {}) self.correction: dict[str, Any] = self._config["energy"].get("correction", {}) + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + @property def ntraces(self) -> int: """Property returning the number of traces. diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index ac26f0e7..fed52473 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -79,8 +79,8 @@ def __init__( self._config = config - self.verbose = verbose - set_verbosity(logger, self.verbose) + self._verbose = verbose + set_verbosity(logger, self._verbose) self.image: np.ndarray = None self.img_ndim: int = None @@ -132,6 +132,25 @@ def __init__( self._state: int = 0 + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + @property def features(self) -> dict: """Dictionary of detected features for the symmetrization process. @@ -1241,7 +1260,7 @@ def apply_func(apply: bool): # noqa: ARG001 transformations["creation_date"] = datetime.now().timestamp() self.transformations = transformations - if self.verbose: + if self._verbose: plt.figure() subs = 20 plt.title("Final Deformation field") diff --git a/sed/core/processor.py b/sed/core/processor.py index ea3ac37e..e931d0a9 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -117,10 +117,10 @@ def __init__( logger.debug(f"Use {num_cores} cores for processing.") if verbose is None: - self.verbose = self._config["core"].get("verbose", True) + self._verbose = self._config["core"].get("verbose", True) else: - self.verbose = verbose - set_verbosity(logger, self.verbose) + self._verbose = verbose + set_verbosity(logger, self._verbose) self._dataframe: pd.DataFrame | ddf.DataFrame = None self._timed_dataframe: pd.DataFrame | ddf.DataFrame = None @@ -148,17 +148,17 @@ def __init__( verbose=verbose, ), config=self._config, - verbose=self.verbose, + verbose=self._verbose, ) self.mc = MomentumCorrector( config=self._config, - verbose=self.verbose, + verbose=self._verbose, ) self.dc = DelayCalibrator( config=self._config, - verbose=self.verbose, + verbose=self._verbose, ) self.use_copy_tool = self._config.get("core", {}).get( @@ -227,6 +227,29 @@ def _repr_html_(self): # """Provides an overview panel with plots of different data attributes.""" # self.view_event_histogram(dfpid=2, backend="matplotlib") + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + self.mc.verbose = verbose + self.ec.verbose = verbose + self.dc.verbose = verbose + self.loader.verbose = verbose + @property def dataframe(self) -> pd.DataFrame | ddf.DataFrame: """Accessor to the underlying dataframe. @@ -629,7 +652,7 @@ def generate_splinewarp( self.mc.spline_warp_estimate(use_center=use_center, **kwds) - if self.mc.slice is not None and self.verbose: + if self.mc.slice is not None and self._verbose: print("Original slice with reference features") self.mc.view(annotated=True, backend="bokeh", crosshair=True) @@ -1334,7 +1357,7 @@ def calibrate_energy_axis( energy_scale=energy_scale, **kwds, ) - if self.verbose: + if self._verbose: self.ec.view( traces=self.ec.traces_normed, xaxis=self.ec.calibration["axis"], diff --git a/sed/loader/base/loader.py b/sed/loader/base/loader.py index 0f898e09..d2848d23 100644 --- a/sed/loader/base/loader.py +++ b/sed/loader/base/loader.py @@ -42,7 +42,25 @@ def __init__( self.files: list[str] = [] self.runs: list[str] = [] self.metadata: dict[Any, Any] = {} - self.verbose = verbose + self._verbose = verbose + + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose @abstractmethod def read_dataframe( diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index b17621fd..9b3524bc 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -54,12 +54,31 @@ def __init__(self, config: dict, verbose: bool = True) -> None: """ super().__init__(config=config, verbose=verbose) - set_verbosity(logger, self.verbose) + set_verbosity(logger, self._verbose) self.instrument: str = self._config["core"].get("instrument", "hextof") # default is hextof self.raw_dir: str = None self.processed_dir: str = None + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + def _initialize_dirs(self) -> None: """ Initializes the directories on Maxwell based on configuration. If paths is provided in diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index c130308a..ebc9a08c 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -570,13 +570,32 @@ def __init__( ): super().__init__(config=config, verbose=verbose) - set_verbosity(logger, self.verbose) + set_verbosity(logger, self._verbose) self.read_timestamps = self._config.get("dataframe", {}).get( "read_timestamps", False, ) + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + def read_dataframe( self, files: str | Sequence[str] = None, diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 1c6d625e..a77e8ed6 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -56,7 +56,7 @@ class SXPLoader(BaseLoader): def __init__(self, config: dict, verbose: bool = True) -> None: super().__init__(config=config, verbose=verbose) - set_verbosity(logger, self.verbose) + set_verbosity(logger, self._verbose) self.multi_index = ["trainId", "pulseId", "electronId"] self.index_per_electron: MultiIndex = None @@ -66,6 +66,25 @@ def __init__(self, config: dict, verbose: bool = True) -> None: self.raw_dir: str = None self.processed_dir: str = None + @property + def verbose(self) -> bool: + """Accessor to the verbosity flag. + + Returns: + bool: Verbosity flag. + """ + return self._verbose + + @verbose.setter + def verbose(self, verbose: bool): + """Setter for the verbosity. + + Args: + verbose (bool): Option to turn on verbose output. Sets loglevel to INFO. + """ + self._verbose = verbose + set_verbosity(logger, self._verbose) + def _initialize_dirs(self): """ Initializes the paths based on the configuration. From 64848b900f4f65293d7a81a5e1229382c5b66f55 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 31 Jul 2024 17:31:35 +0200 Subject: [PATCH 193/300] adding this flag due to https://github.com/astral-sh/ruff/issues/5434 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23dd1b35..43fe23a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: rev: v3.16.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py39-plus, --keep-runtime-typing] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: From eb5ee3f6f65b8f2c6860e21281aa5fd0264b8d7b Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Wed, 31 Jul 2024 19:45:40 +0200 Subject: [PATCH 194/300] first pydantic model for config --- .pre-commit-config.yaml | 2 +- poetry.lock | 146 +++++++++++++++-- pyproject.toml | 1 + sed/config/config_model.py | 224 +++++++++++++++++++++++++++ sed/config/default.yaml | 43 ++--- sed/config/flash_example_config.yaml | 131 +++++++--------- sed/core/config.py | 9 +- 7 files changed, 445 insertions(+), 111 deletions(-) create mode 100644 sed/config/config_model.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43fe23a0..23dd1b35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: rev: v3.16.0 hooks: - id: pyupgrade - args: [--py39-plus, --keep-runtime-typing] + args: [--py39-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.3 hooks: diff --git a/poetry.lock b/poetry.lock index f1b02cae..971e9be1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -40,6 +40,17 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "3.7.1" @@ -2906,12 +2917,12 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] @@ -2999,7 +3010,6 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -3013,14 +3023,12 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -3031,9 +3039,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3455,6 +3463,126 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyerfa" version = "2.0.1.4" diff --git a/pyproject.toml b/pyproject.toml index 75e4892b..8081328b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ jupyter = {version = ">=1.0.0", optional = true} ipykernel = {version = ">=6.9.1", optional = true} jupyterlab = {version = "^3.4.0", optional = true} jupyterlab-h5web = {version = "^8.0.0", extras = ["full"]} +pydantic = "^2.8.2" [tool.poetry.extras] notebook = ["jupyter", "ipykernel", "jupyterlab", "jupyterlab-h5web"] diff --git a/sed/config/config_model.py b/sed/config/config_model.py new file mode 100644 index 00000000..b500e5b3 --- /dev/null +++ b/sed/config/config_model.py @@ -0,0 +1,224 @@ +from collections.abc import Sequence +from typing import Literal +from typing import Optional +from typing import Union + +from pydantic import BaseModel +from pydantic import DirectoryPath +from pydantic import field_validator +from pydantic import FilePath +from pydantic import HttpUrl +from pydantic import NewPath +from pydantic import SecretStr +from typing_extensions import NotRequired +from typing_extensions import TypedDict + +from sed.loader.loader_interface import get_names_of_all_loaders + +## Best to not use futures annotations with pydantic models +## https://github.com/astral-sh/ruff/issues/5434 + + +class Paths(BaseModel): + raw: DirectoryPath + processed: Union[DirectoryPath, NewPath] + + +class CoreModel(BaseModel): + loader: str = "generic" + paths: Optional[Paths] = None + num_cores: int = 4 + year: Optional[int] = None + beamtime_id: Optional[int] = None + instrument: Optional[str] = None + + @field_validator("loader") + @classmethod + def validate_loader(cls, v: str) -> str: + """Checks if the loader is one of the valid ones""" + names = get_names_of_all_loaders() + if v not in names: + raise ValueError(f"Invalid loader {v}. Available loaders are: {names}") + return v + + +class ColumnsModel(BaseModel): + x: str = "X" + y: str = "Y" + tof: str = "t" + tof_ns: str = "t_ns" + corrected_x: str = "Xm" + corrected_y: str = "Ym" + corrected_tof: str = "tm" + kx: str = "kx" + ky: str = "ky" + energy: str = "energy" + delay: str = "delay" + adc: str = "ADC" + bias: str = "sampleBias" + timestamp: str = "timeStamp" + + +class subChannelModel(TypedDict): + slice: int + dtype: NotRequired[str] + + +# Either channels is changed to not be dict or we use TypedDict +# However TypedDict can't accept default values +class ChannelModel(TypedDict): + format: Literal["per_train", "per_electron", "per_pulse", "per_file"] + dataset_key: str + index_key: NotRequired[str] + slice: NotRequired[int] + dtype: NotRequired[str] + sub_channels: NotRequired[dict[str, subChannelModel]] + + +class Dataframe(BaseModel): + columns: ColumnsModel = ColumnsModel() + units: Optional[dict[str, str]] = None + # Since channels are not fixed, we use a TypedDict to represent them. + channels: Optional[dict[str, ChannelModel]] = None + + tof_binwidth: float = 4.125e-12 + tof_binning: int = 1 + adc_binning: int = 1 + jitter_cols: Sequence[str] = ["@x", "@y", "@tof"] + jitter_amps: Union[float, Sequence[float]] = 0.5 + timed_dataframe_unit_time: float = 0.001 + # flash specific settings + forward_fill_iterations: Optional[int] = None + ubid_offset: Optional[int] = None + split_sector_id_from_dld_time: Optional[bool] = None + sector_id_reserved_bits: Optional[int] = None + sector_delays: Optional[Sequence[int]] = None + + +class BinningModel(BaseModel): + hist_mode: str = "numba" + mode: str = "fast" + pbar: bool = True + threads_per_worker: int = 4 + threadpool_API: str = "blas" + + +class HistogramModel(BaseModel): + bins: Sequence[int] = [80, 80, 80] + axes: Sequence[str] = ["@x", "@y", "@tof"] + ranges: Sequence[Sequence[int]] = [[0, 1800], [0, 1800], [0, 150000]] + + +class StaticModel(BaseModel): + """Static configuration settings that shouldn't be changed by users.""" + + # flash specific settings + stream_name_prefixes: Optional[dict] = None + stream_name_postfixes: Optional[dict] = None + beamtime_dir: Optional[dict] = None + + +class EnergyCalibrationModel(BaseModel): + d: float + t0: float + E0: float + energy_scale: str + + +class EnergyCorrectionModel(BaseModel): + correction_type: str = "Lorentzian" + amplitude: float + center: Sequence[float] + gamma: float + sigma: float + diameter: float + + +class EnergyModel(BaseModel): + bins: int = 1000 + ranges: Sequence[int] = [100000, 150000] + normalize: bool = True + normalize_span: int = 7 + normalize_order: int = 1 + fastdtw_radius: int = 2 + peak_window: int = 7 + calibration_method: str = "lmfit" + energy_scale: str = "kinetic" + tof_fermi: int = 132250 + tof_width: Sequence[int] = [-600, 1000] + x_width: Sequence[int] = [-20, 20] + y_width: Sequence[int] = [-20, 20] + color_clip: int = 300 + calibration: Optional[EnergyCalibrationModel] = None + correction: Optional[EnergyCorrectionModel] = None + + +class MomentumCalibrationModel(BaseModel): + kx_scale: float + ky_scale: float + x_center: float + y_center: float + rstart: float + cstart: float + rstep: float + cstep: float + + +class MomentumCorrectionModel(BaseModel): + feature_points: Sequence[Sequence[float]] + rotation_symmetry: int + include_center: bool + use_center: bool + + +class MomentumModel(BaseModel): + axes: Sequence[str] = ["@x", "@y", "@tof"] + bins: Sequence[int] = [512, 512, 300] + ranges: Sequence[Sequence[int]] = [[-256, 1792], [-256, 1792], [132000, 138000]] + detector_ranges: Sequence[Sequence[int]] = [[0, 2048], [0, 2048]] + center_pixel: Sequence[int] = [256, 256] + sigma: int = 5 + fwhm: int = 8 + sigma_radius: int = 1 + calibration: Optional[MomentumCalibrationModel] = None + correction: Optional[MomentumCorrectionModel] = None + + +class DelayModel(BaseModel): + adc_range: Sequence[int] = [1900, 25600] + time0: int = 0 + flip_time_axis: bool = False + p1_key: Optional[str] = None + p2_key: Optional[str] = None + p3_key: Optional[str] = None + + +class MetadataModel(BaseModel): + archiver_url: Optional[HttpUrl] = None + token: Optional[SecretStr] = None + epics_pvs: Optional[Sequence[str]] = None + fa_in_channel: Optional[str] = None + fa_hor_channel: Optional[str] = None + ca_in_channel: Optional[str] = None + aperture_config: Optional[dict] = None + lens_mode_config: Optional[dict] = None + + +class NexusModel(BaseModel): + reader: str # prob good to have validation here + # Currently only NXmpes definition is supported + definition: Literal["NXmpes"] + input_files: Sequence[FilePath] + + +class ConfigModel(BaseModel): + core: CoreModel + dataframe: Dataframe + energy: EnergyModel + momentum: MomentumModel + delay: DelayModel + binning: BinningModel + histogram: HistogramModel + metadata: Optional[MetadataModel] = None + nexus: Optional[NexusModel] = None + static: Optional[StaticModel] = None diff --git a/sed/config/default.yaml b/sed/config/default.yaml index b047d8a8..d6c15956 100644 --- a/sed/config/default.yaml +++ b/sed/config/default.yaml @@ -3,32 +3,23 @@ core: loader: generic dataframe: - # dataframe column containing x coordinates - x_column: "X" - # dataframe column containing y coordinates - y_column: "Y" - # dataframe column containing time-of-flight data - tof_column: "t" - # dataframe column containing time-of-flight data in nanoseconds - tof_ns_column: "t_ns" - # dataframe column containing analog-to-digital data - adc_column: "ADC" - # dataframe column containing bias voltage data - bias_column: "sampleBias" - # dataframe column containing corrected x coordinates - corrected_x_column: "Xm" - # dataframe column containing corrected y coordinates - corrected_y_column: "Ym" - # dataframe column containing corrected time-of-flight data - corrected_tof_column: "tm" - # dataframe column containing kx coordinates - kx_column: "kx" - # dataframe column containing ky coordinates - ky_column: "ky" - # dataframe column containing energy data - energy_column: "energy" - # dataframe column containing delay data - delay_column: "delay" + # Column settings + columns: + x: X # dataframe column containing x coordinates + y: Y # dataframe column containing y coordinates + tof: t # dataframe column containing time-of-flight data + tof_ns: t_ns # dataframe column containing time-of-flight data in nanoseconds + corrected_x: Xm # dataframe column containing corrected x coordinates + corrected_y: Ym # dataframe column containing corrected y coordinates + corrected_tof: tm # dataframe column containing corrected time-of-flight data + kx: kx # dataframe column containing kx coordinates + ky: ky # dataframe column containing ky coordinates + energy: energy # dataframe column containing energy data + delay: delay # dataframe column containing delay data + adc: ADC # dataframe column containing analog-to-digital data + bias: sampleBias # dataframe column containing bias voltage data + timestamp: timeStamp # dataframe column containing timestamp data + # time length of a base time-of-flight bin in s tof_binwidth: 4.125e-12 # Binning factor of the tof_column-data compared to tof_binwidth diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index ccc7dcac..03867513 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -1,9 +1,17 @@ # This file contains the default configuration for the flash loader. +# The paths to the raw and parquet data directories. If these are not +# provided, the loader will try to find the data based on year beamtimeID etc +paths: + # location of the raw data. + raw: "" + # location of the intermediate parquet files. + processed: "" + core: # defines the loader - loader: flash - # Since this will run on maxwell most probably, we have a lot of cores at our disposal + loader: 'flash' + # # Since this will run on maxwell most probably, we have a lot of cores at our disposal num_cores: 100 # the beamline where experiment took place beamline: pg2 @@ -14,14 +22,6 @@ core: # the instrument used instrument: hextof # hextof, wespe, etc - # The paths to the raw and parquet data directories. If these are not - # provided, the loader will try to find the data based on year beamtimeID etc - paths: - # location of the raw data. - raw: "" - # location of the intermediate parquet files. - processed: "" - binning: # Histogram computation mode to use. hist_mode: "numba" @@ -35,57 +35,39 @@ binning: threadpool_API: "blas" dataframe: - # The name of the DAQ system to use. Necessary to resolve the filenames/paths. - daq: fl1user3 - # The offset correction to the pulseId - ubid_offset: 5 - - # the number of iterations to fill the pulseId forward. - forward_fill_iterations: 2 - # if true, removes the 3 bits reserved for dldSectorID from the dldTimeSteps column - split_sector_id_from_dld_time: True - # bits reserved for dldSectorID in the dldTimeSteps column - sector_id_reserved_bits: 3 - # dataframe column containing x coordinates - x_column: dldPosX - # dataframe column containing corrected x coordinates - corrected_x_column: "X" - # dataframe column containing kx coordinates - kx_column: "kx" - # dataframe column containing y coordinates - y_column: dldPosY - # dataframe column containing corrected y coordinates - corrected_y_column: "Y" - # dataframe column containing kx coordinates - ky_column: "ky" - # dataframe column containing time-of-flight data - tof_column: dldTimeSteps - # dataframe column containing time-of-flight data in ns - tof_ns_column: dldTime - # dataframe column containing corrected time-of-flight data - corrected_tof_column: "tm" - # the time stamp column - time_stamp_alias: timeStamp - # auxiliary channel alias - aux_alias: dldAux - # aux subchannels alias - aux_subchannels_alias: dldAuxChannels - # time length of a base time-of-flight bin in seconds - tof_binwidth: 2.0576131995767355E-11 - # binning parameter for time-of-flight data. - tof_binning: 8 - # dataframe column containing sector ID. obtained from dldTimeSteps column - sector_id_column: dldSectorID - sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] - # the delay stage column - delay_column: delayStage - # the corrected pump-probe time axis - corrected_delay_column: pumpProbeTime - # the columns to be used for jitter correction - jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] - + daq: fl1user3 # DAQ system name to resolve filenames/paths + ubid_offset: 5 # Offset correction to the pulseId + forward_fill_iterations: 2 # Number of iterations to fill the pulseId forward + split_sector_id_from_dld_time: True # Remove reserved bits for dldSectorID from dldTimeSteps column + sector_id_reserved_bits: 3 # Bits reserved for dldSectorID in the dldTimeSteps column + sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] # Sector delays + + # Time and binning settings + tof_binwidth: 2.0576131995767355E-11 # Base time-of-flight bin width in seconds + tof_binning: 8 # Binning parameter for time-of-flight data + + # Columns used for jitter correction + jitter_columns: [dldPosX, dldPosY, dldTimeSteps] + + # Column settings + columns: + x: dldPosX + corrected_x: X + kx: kx + y: dldPosY + corrected_y: Y + ky: ky + tof: dldTimeSteps + tof_ns: dldTime + corrected_tof: tm + timestamp: timeStamp + auxiliary: dldAux + sector_id: dldSectorID + delay: delayStage + corrected_delay: pumpProbeTime + + # These are the units of the columns units: - # These are the units of the columns dldPosX: 'step' dldPosY: 'step' dldTimeSteps: 'step' @@ -102,7 +84,7 @@ dataframe: kx: '1/A' ky: '1/A' - # The channels to load. + # The channels to load from the raw data. The channels have the following structure: # channels have the following structure: # : # format: per_pulse/per_electron/per_train @@ -164,7 +146,7 @@ dataframe: index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - subChannels: + sub_channels: sampleBias: slice: 0 dtype: float32 @@ -215,8 +197,20 @@ dataframe: index_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/index" dataset_key: "/zraw/FLASH.SYNC/LASER.LOCK.EXP/F1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/value" +# metadata collection from scicat +# metadata: +# scicat_url: +# scicat_token: + +# The nexus collection routine shall be finalized soon for both instruments +# nexus: +# reader: "flash" +# definition: "NXmpes" +# input_files: ["NXmpes_config_HEXTOF_light.json"] + +# (Not to be changed by user) +static: # The prefixes of the stream names for different DAQ systems for parsing filenames - # (Not to be changed by user) stream_name_prefixes: pbd: "GMD_DATA_gmd_data" pbd2: "FL2PhotDiag_pbd2_gmd_data" @@ -230,14 +224,3 @@ dataframe: # (Not to be changed by user) beamtime_dir: pg2: "/asap3/flash/gpfs/pg2/" - -# metadata collection from scicat -# metadata: -# scicat_url: -# scicat_token: - -# The nexus collection routine shall be finalized soon for both instruments -# nexus: -# reader: "flash" -# definition: "NXmpes" -# input_files: ["NXmpes_config_HEXTOF_light.json"] diff --git a/sed/core/config.py b/sed/core/config.py index 22e60fe5..8628352c 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -13,6 +13,7 @@ from platformdirs import user_config_path from sed.core.logging import setup_logging +from sed.config.config_model import ConfigModel package_dir = os.path.dirname(find_spec("sed").origin) @@ -29,6 +30,7 @@ def parse_config( system_config: dict | str = None, default_config: (dict | str) = f"{package_dir}/config/default.yaml", verbose: bool = True, + model: bool = False, ) -> dict: """Load the config dictionary from a file, or pass the provided config dictionary. The content of the loaded config dictionary is then completed from a set of pre-configured @@ -55,6 +57,7 @@ def parse_config( or file path. The loaded dictionary is completed with the default values. Defaults to *package_dir*/config/default.yaml". verbose (bool, optional): Option to report loaded config files. Defaults to True. + model (bool, optional): Option to return the config model instead of the dictionary. Raises: TypeError: Raised if the provided file is neither *json* nor *yaml*. FileNotFoundError: Raised if the provided file is not found. @@ -141,7 +144,11 @@ def parse_config( base_dictionary=default_dict, ) - return config_dict + # Run the config through the ConfigModel to ensure it is valid + config_model = ConfigModel(**config_dict) + if model: + return config_model + return config_model.model_dump() def load_config(config_path: str) -> dict: From 600ef5564c8baed71888fbdc2181048753fd85db Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 14 Sep 2024 21:19:36 +0200 Subject: [PATCH 195/300] without typeddict --- sed/config/config_model.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index b500e5b3..e70c4342 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -5,13 +5,12 @@ from pydantic import BaseModel from pydantic import DirectoryPath +from pydantic import Field from pydantic import field_validator from pydantic import FilePath from pydantic import HttpUrl from pydantic import NewPath from pydantic import SecretStr -from typing_extensions import NotRequired -from typing_extensions import TypedDict from sed.loader.loader_interface import get_names_of_all_loaders @@ -59,27 +58,25 @@ class ColumnsModel(BaseModel): timestamp: str = "timeStamp" -class subChannelModel(TypedDict): - slice: int - dtype: NotRequired[str] - - -# Either channels is changed to not be dict or we use TypedDict -# However TypedDict can't accept default values -class ChannelModel(TypedDict): +class ChannelModel(BaseModel): format: Literal["per_train", "per_electron", "per_pulse", "per_file"] dataset_key: str - index_key: NotRequired[str] - slice: NotRequired[int] - dtype: NotRequired[str] - sub_channels: NotRequired[dict[str, subChannelModel]] + index_key: Optional[str] = None + slice: Optional[int] = None + dtype: Optional[str] = None + + class subChannelModel(BaseModel): + slice: int + dtype: Optional[str] = None + + sub_channels: Optional[dict[str, subChannelModel]] = None class Dataframe(BaseModel): columns: ColumnsModel = ColumnsModel() units: Optional[dict[str, str]] = None # Since channels are not fixed, we use a TypedDict to represent them. - channels: Optional[dict[str, ChannelModel]] = None + channels: dict[str, ChannelModel] = Field(default_factory=dict) tof_binwidth: float = 4.125e-12 tof_binning: int = 1 From 24998df3a1b3d81f133d12949ebdaac33cf61040 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 15 Sep 2024 01:38:41 +0200 Subject: [PATCH 196/300] remove defaults --- sed/config/config_model.py | 110 ++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index e70c4342..f7626908 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -1,3 +1,4 @@ +"""Pydantic model to validate the config for SED package.""" from collections.abc import Sequence from typing import Literal from typing import Optional @@ -42,20 +43,20 @@ def validate_loader(cls, v: str) -> str: class ColumnsModel(BaseModel): - x: str = "X" - y: str = "Y" - tof: str = "t" - tof_ns: str = "t_ns" - corrected_x: str = "Xm" - corrected_y: str = "Ym" - corrected_tof: str = "tm" - kx: str = "kx" - ky: str = "ky" - energy: str = "energy" - delay: str = "delay" - adc: str = "ADC" - bias: str = "sampleBias" - timestamp: str = "timeStamp" + x: str + y: str + tof: str + tof_ns: str + corrected_x: str + corrected_y: str + corrected_tof: str + kx: str + ky: str + energy: str + delay: str + adc: str + bias: str + timestamp: str class ChannelModel(BaseModel): @@ -75,15 +76,14 @@ class subChannelModel(BaseModel): class Dataframe(BaseModel): columns: ColumnsModel = ColumnsModel() units: Optional[dict[str, str]] = None - # Since channels are not fixed, we use a TypedDict to represent them. channels: dict[str, ChannelModel] = Field(default_factory=dict) - - tof_binwidth: float = 4.125e-12 - tof_binning: int = 1 - adc_binning: int = 1 - jitter_cols: Sequence[str] = ["@x", "@y", "@tof"] - jitter_amps: Union[float, Sequence[float]] = 0.5 - timed_dataframe_unit_time: float = 0.001 + # other settings + tof_binwidth: float + tof_binning: int + adc_binning: int + jitter_cols: Sequence[str] + jitter_amps: Union[float, Sequence[float]] + timed_dataframe_unit_time: float # flash specific settings forward_fill_iterations: Optional[int] = None ubid_offset: Optional[int] = None @@ -93,17 +93,17 @@ class Dataframe(BaseModel): class BinningModel(BaseModel): - hist_mode: str = "numba" - mode: str = "fast" - pbar: bool = True - threads_per_worker: int = 4 - threadpool_API: str = "blas" + hist_mode: str + mode: str + pbar: bool + threads_per_worker: int + threadpool_API: str class HistogramModel(BaseModel): - bins: Sequence[int] = [80, 80, 80] - axes: Sequence[str] = ["@x", "@y", "@tof"] - ranges: Sequence[Sequence[int]] = [[0, 1800], [0, 1800], [0, 150000]] + bins: Sequence[int] + axes: Sequence[str] + ranges: Sequence[Sequence[int]] class StaticModel(BaseModel): @@ -123,7 +123,7 @@ class EnergyCalibrationModel(BaseModel): class EnergyCorrectionModel(BaseModel): - correction_type: str = "Lorentzian" + correction_type: str amplitude: float center: Sequence[float] gamma: float @@ -132,20 +132,20 @@ class EnergyCorrectionModel(BaseModel): class EnergyModel(BaseModel): - bins: int = 1000 - ranges: Sequence[int] = [100000, 150000] - normalize: bool = True - normalize_span: int = 7 - normalize_order: int = 1 - fastdtw_radius: int = 2 - peak_window: int = 7 - calibration_method: str = "lmfit" - energy_scale: str = "kinetic" - tof_fermi: int = 132250 - tof_width: Sequence[int] = [-600, 1000] - x_width: Sequence[int] = [-20, 20] - y_width: Sequence[int] = [-20, 20] - color_clip: int = 300 + bins: int + ranges: Sequence[int] + normalize: bool + normalize_span: int + normalize_order: int + fastdtw_radius: int + peak_window: int + calibration_method: str + energy_scale: str + tof_fermi: int + tof_width: Sequence[int] + x_width: Sequence[int] + y_width: Sequence[int] + color_clip: int calibration: Optional[EnergyCalibrationModel] = None correction: Optional[EnergyCorrectionModel] = None @@ -169,21 +169,21 @@ class MomentumCorrectionModel(BaseModel): class MomentumModel(BaseModel): - axes: Sequence[str] = ["@x", "@y", "@tof"] - bins: Sequence[int] = [512, 512, 300] - ranges: Sequence[Sequence[int]] = [[-256, 1792], [-256, 1792], [132000, 138000]] - detector_ranges: Sequence[Sequence[int]] = [[0, 2048], [0, 2048]] - center_pixel: Sequence[int] = [256, 256] - sigma: int = 5 - fwhm: int = 8 - sigma_radius: int = 1 + axes: Sequence[str] + bins: Sequence[int] + ranges: Sequence[Sequence[int]] + detector_ranges: Sequence[Sequence[int]] + center_pixel: Sequence[int] + sigma: int + fwhm: int + sigma_radius: int calibration: Optional[MomentumCalibrationModel] = None correction: Optional[MomentumCorrectionModel] = None class DelayModel(BaseModel): - adc_range: Sequence[int] = [1900, 25600] - time0: int = 0 + adc_range: Sequence[int] + time0: int flip_time_axis: bool = False p1_key: Optional[str] = None p2_key: Optional[str] = None From bea6079fe1940e93387d10cf360b357d49cd99da Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 15 Sep 2024 01:49:25 +0200 Subject: [PATCH 197/300] update lock file with pydantic --- poetry.lock | 203 +++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/poetry.lock b/poetry.lock index 971e9be1..67de07be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2917,12 +2917,12 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] [[package]] @@ -3039,9 +3039,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3465,119 +3465,120 @@ files = [ [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, + {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.3" typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.3" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, + {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, + {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, + {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, + {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, + {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, + {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, + {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, + {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, + {file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"}, + {file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"}, + {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"}, + {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"}, + {file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"}, + {file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"}, + {file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"}, + {file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"}, + {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"}, + {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"}, + {file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"}, + {file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"}, + {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 8081328b..134c21d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,11 +38,11 @@ tqdm = ">=4.62.3" xarray = ">=0.20.2" joblib = ">=1.2.0" pyarrow = ">=14.0.1, <17.0" +pydantic = ">=2.8.2" jupyter = {version = ">=1.0.0", optional = true} ipykernel = {version = ">=6.9.1", optional = true} jupyterlab = {version = "^3.4.0", optional = true} jupyterlab-h5web = {version = "^8.0.0", extras = ["full"]} -pydantic = "^2.8.2" [tool.poetry.extras] notebook = ["jupyter", "ipykernel", "jupyterlab", "jupyterlab-h5web"] From c7aac14fbc430ae13e7c6ec4091c87ad39432913 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 15 Sep 2024 20:32:37 +0200 Subject: [PATCH 198/300] nest the models --- sed/config/config_model.py | 101 +++++++++++++++++++++++-------------- sed/core/config.py | 4 +- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index f7626908..658a317e 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -1,5 +1,6 @@ """Pydantic model to validate the config for SED package.""" from collections.abc import Sequence +from datetime import datetime from typing import Literal from typing import Optional from typing import Union @@ -50,6 +51,7 @@ class ColumnsModel(BaseModel): corrected_x: str corrected_y: str corrected_tof: str + corrected_delay: str kx: str ky: str energy: str @@ -66,15 +68,15 @@ class ChannelModel(BaseModel): slice: Optional[int] = None dtype: Optional[str] = None - class subChannelModel(BaseModel): + class subChannel(BaseModel): slice: int dtype: Optional[str] = None - sub_channels: Optional[dict[str, subChannelModel]] = None + sub_channels: Optional[dict[str, subChannel]] = None -class Dataframe(BaseModel): - columns: ColumnsModel = ColumnsModel() +class DataframeModel(BaseModel): + columns: ColumnsModel units: Optional[dict[str, str]] = None channels: dict[str, ChannelModel] = Field(default_factory=dict) # other settings @@ -115,22 +117,6 @@ class StaticModel(BaseModel): beamtime_dir: Optional[dict] = None -class EnergyCalibrationModel(BaseModel): - d: float - t0: float - E0: float - energy_scale: str - - -class EnergyCorrectionModel(BaseModel): - correction_type: str - amplitude: float - center: Sequence[float] - gamma: float - sigma: float - diameter: float - - class EnergyModel(BaseModel): bins: int ranges: Sequence[int] @@ -146,26 +132,24 @@ class EnergyModel(BaseModel): x_width: Sequence[int] y_width: Sequence[int] color_clip: int - calibration: Optional[EnergyCalibrationModel] = None - correction: Optional[EnergyCorrectionModel] = None + class EnergyCalibrationModel(BaseModel): + d: float + t0: float + E0: float + energy_scale: str -class MomentumCalibrationModel(BaseModel): - kx_scale: float - ky_scale: float - x_center: float - y_center: float - rstart: float - cstart: float - rstep: float - cstep: float + calibration: Optional[EnergyCalibrationModel] = None + class EnergyCorrectionModel(BaseModel): + correction_type: str + amplitude: float + center: Sequence[float] + gamma: float + sigma: float + diameter: float -class MomentumCorrectionModel(BaseModel): - feature_points: Sequence[Sequence[float]] - rotation_symmetry: int - include_center: bool - use_center: bool + correction: Optional[EnergyCorrectionModel] = None class MomentumModel(BaseModel): @@ -177,17 +161,56 @@ class MomentumModel(BaseModel): sigma: int fwhm: int sigma_radius: int + + class MomentumCalibrationModel(BaseModel): + kx_scale: float + ky_scale: float + x_center: float + y_center: float + rstart: float + cstart: float + rstep: float + cstep: float + calibration: Optional[MomentumCalibrationModel] = None + + class MomentumCorrectionModel(BaseModel): + feature_points: Sequence[Sequence[float]] + rotation_symmetry: int + include_center: bool + use_center: bool + correction: Optional[MomentumCorrectionModel] = None class DelayModel(BaseModel): adc_range: Sequence[int] - time0: int - flip_time_axis: bool = False + flip_time_axis: bool + # Group keys in the datafile p1_key: Optional[str] = None p2_key: Optional[str] = None p3_key: Optional[str] = None + t0_key: Optional[str] = None + + class Calibration(BaseModel): + creation_date: datetime + adc_range: Sequence[int] + delay_range: Sequence[float] + time0: float + delay_range_mm: Sequence[float] + datafile: FilePath # .h5 extension in filepath + + calibration: Optional[Calibration] = None + + class Offsets(BaseModel): + creation_date: datetime + constant: float + flip_delay_axis: bool + weight: float + preserve_mean: bool + reduction: str + + offsets: Optional[Offsets] = None class MetadataModel(BaseModel): @@ -210,7 +233,7 @@ class NexusModel(BaseModel): class ConfigModel(BaseModel): core: CoreModel - dataframe: Dataframe + dataframe: DataframeModel energy: EnergyModel momentum: MomentumModel delay: DelayModel diff --git a/sed/core/config.py b/sed/core/config.py index 8628352c..bd41fc24 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -12,8 +12,8 @@ import yaml from platformdirs import user_config_path -from sed.core.logging import setup_logging from sed.config.config_model import ConfigModel +from sed.core.logging import setup_logging package_dir = os.path.dirname(find_spec("sed").origin) @@ -31,7 +31,7 @@ def parse_config( default_config: (dict | str) = f"{package_dir}/config/default.yaml", verbose: bool = True, model: bool = False, -) -> dict: +) -> dict | ConfigModel: """Load the config dictionary from a file, or pass the provided config dictionary. The content of the loaded config dictionary is then completed from a set of pre-configured config files in hierarchical order, by adding missing items. These additional config files From c179d9ae0098fc92ead01f9585d19c65932a7b2e Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 15 Sep 2024 22:41:45 +0200 Subject: [PATCH 199/300] add copytool in configmodel and other attrs --- sed/config/config_model.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 658a317e..71840198 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -26,12 +26,18 @@ class Paths(BaseModel): class CoreModel(BaseModel): - loader: str = "generic" + loader: str + verbose: Optional[bool] = None paths: Optional[Paths] = None - num_cores: int = 4 + num_cores: Optional[int] = None year: Optional[int] = None beamtime_id: Optional[int] = None instrument: Optional[str] = None + # TODO: move copy tool to a separate model + use_copy_tool: Optional[bool] = None + copy_tool_source: Optional[DirectoryPath] = None + copy_tool_dest: Optional[DirectoryPath] = None + copy_tool_kwds: Optional[dict] = None @field_validator("loader") @classmethod @@ -48,10 +54,6 @@ class ColumnsModel(BaseModel): y: str tof: str tof_ns: str - corrected_x: str - corrected_y: str - corrected_tof: str - corrected_delay: str kx: str ky: str energy: str @@ -59,6 +61,12 @@ class ColumnsModel(BaseModel): adc: str bias: str timestamp: str + corrected_x: str + corrected_y: str + corrected_tof: str + corrected_delay: Optional[str] = None + sector_id: Optional[str] = None + auxiliary: Optional[str] = None class ChannelModel(BaseModel): From d7cab5f01f004b277bfc8a830a5dfb56f9d0a774 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 15 Sep 2024 22:42:16 +0200 Subject: [PATCH 200/300] update config files to conform to model --- sed/config/default.yaml | 6 ++--- sed/config/mpes_example_config.yaml | 41 +++++++++++------------------ 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/sed/config/default.yaml b/sed/config/default.yaml index d6c15956..3d591926 100644 --- a/sed/config/default.yaml +++ b/sed/config/default.yaml @@ -27,7 +27,7 @@ dataframe: # binning factor used for the adc coordinate adc_binning: 1 # list of columns to apply jitter to. - jitter_cols: ["@x_column", "@y_column", "@tof_column"] + jitter_cols: ["@x", "@y", "@tof"] # Jitter amplitude or list of jitter amplitudes. Should equal half the digital step size of each jitter_column jitter_amps: 0.5 # Time stepping in seconds of the successive events in the timed dataframe @@ -68,7 +68,7 @@ energy: momentum: # binning axes to use for momentum correction/calibration. # Axes names starting with "@" refer to keys in the "dataframe" section - axes: ["@x_column", "@y_column", "@tof_column"] + axes: ["@x", "@y", "@tof"] # Bin numbers used for the respective axes bins: [512, 512, 300] # bin ranges to use (in unbinned detector coordinates) @@ -110,6 +110,6 @@ histogram: bins: [80, 80, 80] # default axes to use for histogram visualization. # Axes names starting with "@" refer to keys in the "dataframe" section - axes: ["@x_column", "@y_column", "@tof_column"] + axes: ["@x", "@y", "@tof"] # default ranges to use for histogram visualization (in unbinned detector coordinates) ranges: [[0, 1800], [0, 1800], [0, 150000]] diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 4848945f..f191c97e 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -25,30 +25,19 @@ dataframe: timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to jitter_cols: ["X", "Y", "t", "ADC"] - # dataframe column containing x coordinates - x_column: "X" - # dataframe column containing y coordinates - y_column: "Y" - # dataframe column containing time-of-flight data - tof_column: "t" - # dataframe column containing analog-to-digital data - adc_column: "ADC" - # dataframe column containing bias voltage data - bias_column: "sampleBias" - # dataframe column containing corrected x coordinates - corrected_x_column: "Xm" - # dataframe column containing corrected y coordinates - corrected_y_column: "Ym" - # dataframe column containing corrected time-of-flight data - corrected_tof_column: "tm" - # dataframe column containing kx coordinates - kx_column: "kx" - # dataframe column containing ky coordinates - ky_column: "ky" - # dataframe column containing energy data - energy_column: "energy" - # dataframe column containing delay data - delay_column: "delay" + columns: + x: X # dataframe column containing x coordinates + y: Y # dataframe column containing y coordinates + tof: t # dataframe column containing time-of-flight data + adc: ADC # dataframe column containing analog-to-digital data + bias: sampleBias # dataframe column containing bias voltage data + corrected_x: Xm # dataframe column containing corrected x coordinates + corrected_y: Ym # dataframe column containing corrected y coordinates + corrected_tof: tm # dataframe column containing corrected time-of-flight data + kx: kx # dataframe column containing kx coordinates + ky: ky # dataframe column containing ky coordinates + energy: energy # dataframe column containing energy data + delay: delay # dataframe column containing delay data # time length of a base time-of-flight bin in ns tof_binwidth: 4.125e-12 # Binning factor of the tof_column-data compared to tof_binwidth @@ -155,7 +144,7 @@ energy: momentum: # binning axes to use for momentum correction/calibration. # Axes names starting with "@" refer to keys in the "dataframe" section - axes: ["@x_column", "@y_column", "@tof_column"] + axes: ["@x", "@y", "@tof"] # Bin numbers used for the respective axes bins: [512, 512, 300] # bin ranges to use (in unbinned detector coordinates) @@ -226,7 +215,7 @@ histogram: bins: [80, 80, 80, 80] # default axes to use for histogram visualization. # Axes names starting with "@" refer to keys in the "dataframe" section - axes: ["@x_column", "@y_column", "@tof_column", "@adc_column"] + axes: ["@x", "@y", "@tof", "@adc"] # default ranges to use for histogram visualization (in unbinned detector coordinates) ranges: [[0, 1800], [0, 1800], [256000, 276000], [0, 32000]] From 75f1200765f81c0963a4136469828f28a35a0ae9 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 16 Sep 2024 03:50:43 +0200 Subject: [PATCH 201/300] use configmodel in processor class --- sed/core/processor.py | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/sed/core/processor.py b/sed/core/processor.py index e931d0a9..e4d5f550 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -823,8 +823,8 @@ def apply_momentum_correction( - **inv_dfield** (np.ndarray, optional): Inverse deformation field. """ - x_column = self._config["dataframe"]["x_column"] - y_column = self._config["dataframe"]["y_column"] + x_column = self._config["dataframe"]["columns"]["x"] + y_column = self._config["dataframe"]["columns"]["y"] if self._dataframe is not None: logger.info("Adding corrected X/Y columns to dataframe:") @@ -967,8 +967,8 @@ def apply_momentum_calibration( Defaults to False. **kwds: Keyword args passed to ``MomentumCalibrator.append_k_axis``. """ - x_column = self._config["dataframe"]["x_column"] - y_column = self._config["dataframe"]["y_column"] + x_column = self._config["dataframe"]["columns"]["x"] + y_column = self._config["dataframe"]["columns"]["y"] if self._dataframe is not None: logger.info("Adding kx/ky columns to dataframe:") @@ -1108,7 +1108,7 @@ def apply_energy_correction( **kwds: Keyword args passed to ``EnergyCalibrator.apply_energy_correction()``. """ - tof_column = self._config["dataframe"]["tof_column"] + tof_column = self._config["dataframe"]["columns"]["tof"] if self._dataframe is not None: logger.info("Applying energy correction to dataframe...") @@ -1187,16 +1187,16 @@ def load_bias_series( if binned_data is not None: if isinstance(binned_data, xr.DataArray): if ( - self._config["dataframe"]["tof_column"] not in binned_data.dims - or self._config["dataframe"]["bias_column"] not in binned_data.dims + self._config["dataframe"]["columns"]["tof"] not in binned_data.dims + or self._config["dataframe"]["columns"]["bias"] not in binned_data.dims ): raise ValueError( "If binned_data is provided as an xarray, it needs to contain dimensions " f"'{self._config['dataframe']['tof_column']}' and " f"'{self._config['dataframe']['bias_column']}'!.", ) - tof = binned_data.coords[self._config["dataframe"]["tof_column"]].values - biases = binned_data.coords[self._config["dataframe"]["bias_column"]].values + tof = binned_data.coords[self._config["dataframe"]["columns"]["tof"]].values + biases = binned_data.coords[self._config["dataframe"]["columns"]["bias"]].values traces = binned_data.values[:, :] else: try: @@ -1470,7 +1470,7 @@ def append_energy_axis( **kwds: Keyword args passed to ``EnergyCalibrator.append_energy_axis()``. """ - tof_column = self._config["dataframe"]["tof_column"] + tof_column = self._config["dataframe"]["columns"]["tof"] if self._dataframe is not None: logger.info("Adding energy column to dataframe:") @@ -1536,7 +1536,7 @@ def add_energy_offset( Raises: ValueError: If the energy column is not in the dataframe. """ - energy_column = self._config["dataframe"]["energy_column"] + energy_column = self._config["dataframe"]["columns"]["energy"] if energy_column not in self._dataframe.columns: raise ValueError( f"Energy column {energy_column} not found in dataframe! " @@ -1624,7 +1624,7 @@ def append_tof_ns_axis( **kwds: additional arguments are passed to ``EnergyCalibrator.append_tof_ns_axis()``. """ - tof_column = self._config["dataframe"]["tof_column"] + tof_column = self._config["dataframe"]["columns"]["tof"] if self._dataframe is not None: logger.info("Adding time-of-flight column in nanoseconds to dataframe.") @@ -1671,7 +1671,7 @@ def align_dld_sectors( Defaults to False. **kwds: additional arguments are passed to ``EnergyCalibrator.align_dld_sectors()``. """ - tof_column = self._config["dataframe"]["tof_column"] + tof_column = self._config["dataframe"]["columns"]["tof"] if self._dataframe is not None: logger.info("Aligning 8s sectors of dataframe") @@ -1725,7 +1725,7 @@ def calibrate_delay_axis( Defaults to False. **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``. """ - adc_column = self._config["dataframe"]["adc_column"] + adc_column = self._config["dataframe"]["columns"]["adc"] if adc_column not in self._dataframe.columns: raise ValueError(f"ADC column {adc_column} not found in dataframe, cannot calibrate!") @@ -1841,7 +1841,7 @@ def add_delay_offset( Raises: ValueError: If the delay column is not in the dataframe. """ - delay_column = self._config["dataframe"]["delay_column"] + delay_column = self._config["dataframe"]["columns"]["delay"] if delay_column not in self._dataframe.columns: raise ValueError(f"Delay column {delay_column} not found in dataframe! ") @@ -1964,7 +1964,7 @@ def add_jitter( cols = self._config["dataframe"]["jitter_cols"] for loc, col in enumerate(cols): if col.startswith("@"): - cols[loc] = self._config["dataframe"].get(col.strip("@")) + cols[loc] = self._config["dataframe"]["columns"].get(col.strip("@")) if amps is None: amps = self._config["dataframe"]["jitter_amps"] @@ -2024,7 +2024,7 @@ def add_time_stamped_data( """ time_stamp_column = kwds.pop( "time_stamp_column", - self._config["dataframe"].get("time_stamp_alias", ""), + self._config["dataframe"]["columns"].get("timestamp", ""), ) if time_stamps is None and data is None: @@ -2099,7 +2099,7 @@ def pre_binning( axes = self._config["momentum"]["axes"] for loc, axis in enumerate(axes): if axis.startswith("@"): - axes[loc] = self._config["dataframe"].get(axis.strip("@")) + axes[loc] = self._config["dataframe"]["columns"].get(axis.strip("@")) if bins is None: bins = self._config["momentum"]["bins"] @@ -2333,14 +2333,14 @@ def get_normalization_histogram( self._dataframe.partitions[df_partitions], axis, self._binned.coords[axis].values, - self._config["dataframe"]["time_stamp_alias"], + self._config["dataframe"]["columns"]["timestamp"], ) else: self._normalization_histogram = normalization_histogram_from_timestamps( self._dataframe, axis, self._binned.coords[axis].values, - self._config["dataframe"]["time_stamp_alias"], + self._config["dataframe"]["columns"]["timestamp"], ) else: if df_partitions is not None: @@ -2406,13 +2406,13 @@ def view_event_histogram( axes = list(axes) for loc, axis in enumerate(axes): if axis.startswith("@"): - axes[loc] = self._config["dataframe"].get(axis.strip("@")) + axes[loc] = self._config["dataframe"]["columns"].get(axis.strip("@")) if ranges is None: ranges = list(self._config["histogram"]["ranges"]) for loc, axis in enumerate(axes): - if axis == self._config["dataframe"]["tof_column"]: + if axis == self._config["dataframe"]["columns"]["tof"]: ranges[loc] = np.asarray(ranges[loc]) / self._config["dataframe"]["tof_binning"] - elif axis == self._config["dataframe"]["adc_column"]: + elif axis == self._config["dataframe"]["columns"]["adc"]: ranges[loc] = np.asarray(ranges[loc]) / self._config["dataframe"]["adc_binning"] input_types = map(type, [axes, bins, ranges]) From 8ef58073cbb3b72b3afe8f7ce60084e30a874968 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 16 Sep 2024 15:18:40 +0200 Subject: [PATCH 202/300] update modules to new config --- sed/calibrator/delay.py | 12 +++++------ sed/calibrator/energy.py | 32 ++++++++++++++-------------- sed/calibrator/momentum.py | 32 ++++++++++++++-------------- sed/config/config_model.py | 7 +++++- sed/config/flash_example_config.yaml | 16 ++++++-------- sed/config/mpes_example_config.yaml | 4 ++-- sed/core/config.py | 12 +++++------ sed/dataset/dataset.py | 1 + tests/data/loader/flash/config.yaml | 16 +++++++++++++- tests/test_config.py | 13 ++++++++--- 10 files changed, 85 insertions(+), 60 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 000fe9cf..a15edd15 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -51,10 +51,10 @@ def __init__( self._verbose = verbose set_verbosity(logger, self._verbose) - self.adc_column: str = self._config["dataframe"].get("adc_column", None) - self.delay_column: str = self._config["dataframe"]["delay_column"] - self.corrected_delay_column = self._config["dataframe"].get( - "corrected_delay_column", + self.adc_column: str = config["dataframe"]["columns"]["adc"] + self.delay_column: str = config["dataframe"]["columns"]["delay"] + self.corrected_delay_column = self._config["dataframe"]["columns"].get( + "corrected_delay", self.delay_column, ) self.calibration: dict[str, Any] = self._config["delay"].get("calibration", {}) @@ -102,9 +102,9 @@ def append_delay_axis( df (pd.DataFrame | dask.dataframe.DataFrame): The dataframe where to apply the delay calibration to. adc_column (str, optional): Source column for delay calibration. - Defaults to config["dataframe"]["adc_column"]. + Defaults to config["dataframe"]["columns"]["adc"]. delay_column (str, optional): Destination column for delay calibration. - Defaults to config["dataframe"]["delay_column"]. + Defaults to config["dataframe"]["columns"]["delay"]. calibration (dict, optional): Calibration dictionary with parameters for delay calibration. adc_range (tuple | list | np.ndarray, optional): The range of used diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index e3e7d1ca..83429ed2 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -107,12 +107,12 @@ def __init__( self.peaks: np.ndarray = np.asarray([]) self.calibration: dict[str, Any] = self._config["energy"].get("calibration", {}) - self.tof_column = self._config["dataframe"]["tof_column"] - self.tof_ns_column = self._config["dataframe"].get("tof_ns_column", None) - self.corrected_tof_column = self._config["dataframe"]["corrected_tof_column"] - self.energy_column = self._config["dataframe"]["energy_column"] - self.x_column = self._config["dataframe"]["x_column"] - self.y_column = self._config["dataframe"]["y_column"] + self.tof_column = self._config["dataframe"]["columns"]["tof"] + self.tof_ns_column = self._config["dataframe"]["columns"].get("tof_ns", None) + self.corrected_tof_column = self._config["dataframe"]["columns"]["corrected_tof"] + self.energy_column = self._config["dataframe"]["columns"]["energy"] + self.x_column = self._config["dataframe"]["columns"]["x"] + self.y_column = self._config["dataframe"]["columns"]["y"] self.binwidth: float = self._config["dataframe"]["tof_binwidth"] self.binning: int = self._config["dataframe"]["tof_binning"] self.x_width = self._config["energy"]["x_width"] @@ -121,7 +121,7 @@ def __init__( self.tof_fermi = self._config["energy"]["tof_fermi"] / self.binning self.color_clip = self._config["energy"]["color_clip"] self.sector_delays = self._config["dataframe"].get("sector_delays", None) - self.sector_id_column = self._config["dataframe"].get("sector_id_column", None) + self.sector_id_column = self._config["dataframe"]["columns"].get("sector_id", None) self.offsets: dict[str, Any] = self._config["energy"].get("offsets", {}) self.correction: dict[str, Any] = self._config["energy"].get("correction", {}) @@ -217,7 +217,7 @@ def bin_data( Args: data_files (list[str]): list of file names to bin axes (list[str], optional): bin axes. Defaults to - config["dataframe"]["tof_column"]. + config["dataframe"]["columns"]["tof"]. bins (list[int], optional): number of bins. Defaults to config["energy"]["bins"]. ranges (Sequence[tuple[float, float]], optional): bin ranges. @@ -802,9 +802,9 @@ def append_energy_axis( df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the energy axis calibration to. tof_column (str, optional): Label of the source column. - Defaults to config["dataframe"]["tof_column"]. + Defaults to config["dataframe"]["columns"]["tof"]. energy_column (str, optional): Label of the destination column. - Defaults to config["dataframe"]["energy_column"]. + Defaults to config["dataframe"]["columns"]["energy"]. calibration (dict, optional): Calibration dictionary. If provided, overrides calibration from class or config. Defaults to self.calibration or config["energy"]["calibration"]. @@ -948,9 +948,9 @@ def append_tof_ns_axis( Args: df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to convert. tof_column (str, optional): Name of the column containing the - time-of-flight steps. Defaults to config["dataframe"]["tof_column"]. + time-of-flight steps. Defaults to config["dataframe"]["columns"]["tof"]. tof_ns_column (str, optional): Name of the column to store the - time-of-flight in nanoseconds. Defaults to config["dataframe"]["tof_ns_column"]. + time-of-flight in nanoseconds. Defaults to config["dataframe"]["columns"]["tof_ns"]. binwidth (float, optional): Time-of-flight binwidth in ns. Defaults to config["energy"]["tof_binwidth"]. binning (int, optional): Time-of-flight binning factor. @@ -1381,9 +1381,9 @@ def apply_energy_correction( df (pd.DataFrame | dask.dataframe.DataFrame): The dataframe where to apply the energy correction to. tof_column (str, optional): Name of the source column to convert. - Defaults to config["dataframe"]["tof_column"]. + Defaults to config["dataframe"]["columns"]["tof"]. new_tof_column (str, optional): Name of the destination column to convert. - Defaults to config["dataframe"]["corrected_tof_column"]. + Defaults to config["dataframe"]["columns"]["corrected_tof"]. correction_type (str, optional): Type of correction to apply to the TOF axis. Valid values are: @@ -1494,9 +1494,9 @@ def align_dld_sectors( Args: df (dask.dataframe.DataFrame): Dataframe to use. tof_column (str, optional): Name of the column containing the time-of-flight values. - Defaults to config["dataframe"]["tof_column"]. + Defaults to config["dataframe"]["columns"]["tof"]. sector_id_column (str, optional): Name of the column containing the sector id values. - Defaults to config["dataframe"]["sector_id_column"]. + Defaults to config["dataframe"]["columns"]["sector_id"]. sector_delays (np.ndarray, optional): Array containing the sector delays. Defaults to config["dataframe"]["sector_delays"]. diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index fed52473..f1852d8a 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -123,12 +123,12 @@ def __init__( self.adjust_params: dict[str, Any] = {} self.calibration: dict[str, Any] = self._config["momentum"].get("calibration", {}) - self.x_column = self._config["dataframe"]["x_column"] - self.y_column = self._config["dataframe"]["y_column"] - self.corrected_x_column = self._config["dataframe"]["corrected_x_column"] - self.corrected_y_column = self._config["dataframe"]["corrected_y_column"] - self.kx_column = self._config["dataframe"]["kx_column"] - self.ky_column = self._config["dataframe"]["ky_column"] + self.x_column = self._config["dataframe"]["columns"]["x"] + self.y_column = self._config["dataframe"]["columns"]["y"] + self.corrected_x_column = self._config["dataframe"]["columns"]["corrected_x"] + self.corrected_y_column = self._config["dataframe"]["columns"]["corrected_y"] + self.kx_column = self._config["dataframe"]["columns"]["kx"] + self.ky_column = self._config["dataframe"]["columns"]["ky"] self._state: int = 0 @@ -1749,15 +1749,15 @@ def apply_corrections( df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the distortion correction to. x_column (str, optional): Label of the 'X' column before momentum - distortion correction. Defaults to config["momentum"]["x_column"]. + distortion correction. Defaults to config["dataframe"]["columns"]["x"]. y_column (str, optional): Label of the 'Y' column before momentum - distortion correction. Defaults to config["momentum"]["y_column"]. + distortion correction. Defaults to config["dataframe"]["columns"]["y"]. new_x_column (str, optional): Label of the 'X' column after momentum distortion correction. - Defaults to config["momentum"]["corrected_x_column"]. + Defaults to config["dataframe"]["columns"]["corrected_x"]. new_y_column (str, optional): Label of the 'Y' column after momentum distortion correction. - Defaults to config["momentum"]["corrected_y_column"]. + Defaults to config["dataframe"]["columns"]["corrected_y"]. Returns: tuple[pd.DataFrame | dask.dataframe.DataFrame, dict]: Dataframe with @@ -1898,15 +1898,15 @@ def append_k_axis( df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to apply the distortion correction to. x_column (str, optional): Label of the source 'X' column. - Defaults to config["momentum"]["corrected_x_column"] or - config["momentum"]["x_column"] (whichever is present). + Defaults to config["dataframe"]["columns"]["corrected_x"] or + config["dataframe"]["columns"]["x"] (whichever is present). y_column (str, optional): Label of the source 'Y' column. - Defaults to config["momentum"]["corrected_y_column"] or - config["momentum"]["y_column"] (whichever is present). + Defaults to config["dataframe"]["columns"]["corrected_y"] or + config["dataframe"]["columns"]["y"] (whichever is present). new_x_column (str, optional): Label of the destination 'X' column after - momentum calibration. Defaults to config["momentum"]["kx_column"]. + momentum calibration. Defaults to config["dataframe"]["columns"]["kx"]. new_y_column (str, optional): Label of the destination 'Y' column after - momentum calibration. Defaults to config["momentum"]["ky_column"]. + momentum calibration. Defaults to config["dataframe"]["columns"]["ky"]. calibration (dict, optional): Dictionary containing calibration parameters. Defaults to 'self.calibration' or config["momentum"]["calibration"]. suppress_output (bool, optional): Option to suppress output of diagnostic information. diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 71840198..ae2ef64b 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -6,6 +6,7 @@ from typing import Union from pydantic import BaseModel +from pydantic import ConfigDict from pydantic import DirectoryPath from pydantic import Field from pydantic import field_validator @@ -101,6 +102,8 @@ class DataframeModel(BaseModel): sector_id_reserved_bits: Optional[int] = None sector_delays: Optional[Sequence[int]] = None + # write validator for model so that x_column gets converted to columns: x + class BinningModel(BaseModel): hist_mode: str @@ -113,7 +116,7 @@ class BinningModel(BaseModel): class HistogramModel(BaseModel): bins: Sequence[int] axes: Sequence[str] - ranges: Sequence[Sequence[int]] + ranges: Sequence[tuple[float, float]] class StaticModel(BaseModel): @@ -250,3 +253,5 @@ class ConfigModel(BaseModel): metadata: Optional[MetadataModel] = None nexus: Optional[NexusModel] = None static: Optional[StaticModel] = None + + model_config = ConfigDict(extra="forbid") diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 03867513..185fac78 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -1,13 +1,4 @@ # This file contains the default configuration for the flash loader. - -# The paths to the raw and parquet data directories. If these are not -# provided, the loader will try to find the data based on year beamtimeID etc -paths: - # location of the raw data. - raw: "" - # location of the intermediate parquet files. - processed: "" - core: # defines the loader loader: 'flash' @@ -21,6 +12,13 @@ core: year: 2023 # the instrument used instrument: hextof # hextof, wespe, etc + # The paths to the raw and parquet data directories. If these are not + # provided, the loader will try to find the data based on year beamtimeID etc + # paths: + # # location of the raw data. + # raw: "" + # # location of the intermediate parquet files. + # processed: "" binning: # Histogram computation mode to use. diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index f191c97e..3d1d3e39 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -6,9 +6,9 @@ core: # Option to use the copy tool to mirror data to a local storage location before processing. use_copy_tool: False # path to the root of the source data directory - copy_tool_source: "/path/to/data/" + copy_tool_source: null # "/path/to/data/" # path to the root or the local data storage - copy_tool_dest: "/path/to/localDataStore/" + copy_tool_dest: null # "/path/to/localDataStore/" # optional keywords for the copy tool: copy_tool_kwds: # group id to set for copied files and folders diff --git a/sed/core/config.py b/sed/core/config.py index bd41fc24..df44cfd1 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -30,8 +30,8 @@ def parse_config( system_config: dict | str = None, default_config: (dict | str) = f"{package_dir}/config/default.yaml", verbose: bool = True, - model: bool = False, -) -> dict | ConfigModel: + verify_config: bool = True, +) -> dict: """Load the config dictionary from a file, or pass the provided config dictionary. The content of the loaded config dictionary is then completed from a set of pre-configured config files in hierarchical order, by adding missing items. These additional config files @@ -57,13 +57,13 @@ def parse_config( or file path. The loaded dictionary is completed with the default values. Defaults to *package_dir*/config/default.yaml". verbose (bool, optional): Option to report loaded config files. Defaults to True. - model (bool, optional): Option to return the config model instead of the dictionary. + verify_config (bool, optional): Option to verify config file. Defaults to True. Raises: TypeError: Raised if the provided file is neither *json* nor *yaml*. FileNotFoundError: Raised if the provided file is not found. Returns: - dict: Loaded and possibly completed config dictionary. + ConfigModel: Loaded and possibly completed pydantic config model. """ if config is None: config = {} @@ -144,10 +144,10 @@ def parse_config( base_dictionary=default_dict, ) + if not verify_config: + return config_dict # Run the config through the ConfigModel to ensure it is valid config_model = ConfigModel(**config_dict) - if model: - return config_model return config_model.model_dump() diff --git a/sed/dataset/dataset.py b/sed/dataset/dataset.py index 15bf8b4f..fca7fc83 100644 --- a/sed/dataset/dataset.py +++ b/sed/dataset/dataset.py @@ -55,6 +55,7 @@ def load_datasets_dict() -> dict: system_config={}, default_config=DatasetsManager.json_path["module"], verbose=False, + verify_config=False, ) @staticmethod diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 19e01a2a..d4209636 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -61,7 +61,21 @@ dataframe: sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] - + columns: + x: dldPosX + corrected_x: X + kx: kx + y: dldPosY + corrected_y: Y + ky: ky + tof: dldTimeSteps + tof_ns: dldTime + corrected_tof: tm + timestamp: timeStamp + auxiliary: dldAux + sector_id: dldSectorID + delay: delayStage + corrected_delay: pumpProbeTime units: dldPosX: 'step' dldPosY: 'step' diff --git a/tests/test_config.py b/tests/test_config.py index df875b45..0ab54cbb 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -36,7 +36,7 @@ def test_default_config() -> None: """Test the config loader for the default config.""" - config = parse_config() + config = parse_config(verify_config=False) assert isinstance(config, dict) for key in default_config_keys: assert key in config.keys() @@ -49,7 +49,7 @@ def test_default_config() -> None: def test_load_dict() -> None: """Test the config loader for a dict.""" config_dict = {"test_entry": True} - config = parse_config(config_dict) + config = parse_config(config_dict, verify_config=False) assert isinstance(config, dict) for key in default_config_keys: assert key in config.keys() @@ -69,7 +69,14 @@ def test_load_does_not_modify() -> None: default_dict = {"a": 1, "b": {"c": 13}, "c": {"e": 11}} default_copy = copy.deepcopy(default_dict) - parse_config(config_dict, folder_dict, user_dict, system_dict, default_dict) + parse_config( + config_dict, + folder_dict, + user_dict, + system_dict, + default_dict, + verify_config=False, + ) assert config_dict == config_copy assert folder_dict == folder_copy assert user_dict == user_copy From 02aea0aed5a39e8fc842e3d06208342835821cac Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 16 Sep 2024 16:03:51 +0200 Subject: [PATCH 203/300] fix some config problems --- sed/config/config_model.py | 21 +++++++++++++-------- sed/loader/mpes/loader.py | 2 +- tests/calibrator/test_delay.py | 7 +++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index ae2ef64b..2356d4dd 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -8,7 +8,6 @@ from pydantic import BaseModel from pydantic import ConfigDict from pydantic import DirectoryPath -from pydantic import Field from pydantic import field_validator from pydantic import FilePath from pydantic import HttpUrl @@ -87,7 +86,7 @@ class subChannel(BaseModel): class DataframeModel(BaseModel): columns: ColumnsModel units: Optional[dict[str, str]] = None - channels: dict[str, ChannelModel] = Field(default_factory=dict) + channels: Optional[dict[str, ChannelModel]] = None # other settings tof_binwidth: float tof_binning: int @@ -214,12 +213,18 @@ class Calibration(BaseModel): calibration: Optional[Calibration] = None class Offsets(BaseModel): - creation_date: datetime - constant: float - flip_delay_axis: bool - weight: float - preserve_mean: bool - reduction: str + creation_date: Optional[datetime] = None + constant: Optional[float] = None + flip_delay_axis: Optional[bool] = False + + ## This seems rather complicated way to define offsets, + # inconsistent in how args vs config are for add_offsets + class OffsetColumn(BaseModel): + weight: float + preserve_mean: bool + reduction: Optional[str] = None + + columns: Optional[dict[str, OffsetColumn]] = None offsets: Optional[Offsets] = None diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index ebc9a08c..eaf296f0 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -72,7 +72,7 @@ def hdf5_to_dataframe( electron_channels = [] column_names = [] - + print("Print values: ", channels) for name, channel in channels.items(): if channel["format"] == "per_electron": if channel["dataset_key"] in test_proc: diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index d7677296..a81542e2 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -136,9 +136,8 @@ def test_delay_parameters_from_delay_range_mm() -> None: "offsets": { "constant": 1, "flip_delay_axis": True, - "bam": { - "weight": 0.001, - "preserve_mean": False, + "columns": { + "bam": {"weight": 0.001, "preserve_mean": False}, }, }, }, @@ -201,7 +200,7 @@ def test_add_offset_from_dict(df=test_dataframe) -> None: """test that the timing offset is corrected for correctly from config""" cfg_ = cfg.copy() offsets = cfg["delay"]["offsets"] # type:ignore - offsets["bam"].pop("weight") + offsets["columns"]["bam"].pop("weight") offsets["flip_delay_axis"] = False cfg_.pop("delay") config = parse_config( From d84f6e49a09b463bffc5d1a5102a18d7c33b8cba Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 7 Oct 2024 14:15:19 +0200 Subject: [PATCH 204/300] update lockfile --- poetry.lock | 592 +++++++++++++++++++++++++++------------------------- 1 file changed, 312 insertions(+), 280 deletions(-) diff --git a/poetry.lock b/poetry.lock index 67de07be..9ec11c08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -196,13 +196,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] [[package]] name = "asteval" -version = "1.0.4" +version = "1.0.5" description = "Safe, minimalistic evaluator of python expression using ast module" optional = false python-versions = ">=3.8" files = [ - {file = "asteval-1.0.4-py3-none-any.whl", hash = "sha256:7a88bfd0dd1eabdf20bb4995904df742cecf876f7f9e700f22231abf4e34d50c"}, - {file = "asteval-1.0.4.tar.gz", hash = "sha256:15e63bd01fce65aded51357f7e1debc6f46100d777c372af11c27c07cb740074"}, + {file = "asteval-1.0.5-py3-none-any.whl", hash = "sha256:082b95312578affc8a6d982f7d92b7ac5de05634985c87e7eedd3188d31149fa"}, + {file = "asteval-1.0.5.tar.gz", hash = "sha256:bac3c8dd6d2b789e959cfec9bb296fb8338eec066feae618c462132701fbc665"}, ] [package.extras] @@ -265,13 +265,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.9.16.0.32.21" +version = "0.2024.10.7.0.32.46" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" files = [ - {file = "astropy_iers_data-0.2024.9.16.0.32.21-py3-none-any.whl", hash = "sha256:adf111e1b596470c4437fa44cf767e56f6d4bc2e93068871fd0b30c73476d430"}, - {file = "astropy_iers_data-0.2024.9.16.0.32.21.tar.gz", hash = "sha256:2ff6fe868a623e616953a432698b05dd6adac9683d21ac780bfbb94e78f7c344"}, + {file = "astropy_iers_data-0.2024.10.7.0.32.46-py3-none-any.whl", hash = "sha256:4b5ce3e3c8ac17632a28a35f4b847d8a8db084509f5f0db3238140dbb5149eae"}, + {file = "astropy_iers_data-0.2024.10.7.0.32.46.tar.gz", hash = "sha256:349ff09294f03112d9391a5ad660135bf2fcef7de411a52c20bcf8598fb029d1"}, ] [package.extras] @@ -891,33 +891,33 @@ test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailu [[package]] name = "debugpy" -version = "1.8.5" +version = "1.8.6" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, - {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, - {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, - {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, - {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, - {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, - {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, - {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, - {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, - {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, - {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, - {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, - {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, - {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, - {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, - {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, - {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, - {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, - {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, - {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, - {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, - {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, + {file = "debugpy-1.8.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b"}, + {file = "debugpy-1.8.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b"}, + {file = "debugpy-1.8.6-cp310-cp310-win32.whl", hash = "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9"}, + {file = "debugpy-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd"}, + {file = "debugpy-1.8.6-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955"}, + {file = "debugpy-1.8.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b"}, + {file = "debugpy-1.8.6-cp311-cp311-win32.whl", hash = "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43"}, + {file = "debugpy-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833"}, + {file = "debugpy-1.8.6-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128"}, + {file = "debugpy-1.8.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972"}, + {file = "debugpy-1.8.6-cp312-cp312-win32.whl", hash = "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c"}, + {file = "debugpy-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f"}, + {file = "debugpy-1.8.6-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:df5dc9eb4ca050273b8e374a4cd967c43be1327eeb42bfe2f58b3cdfe7c68dcb"}, + {file = "debugpy-1.8.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a"}, + {file = "debugpy-1.8.6-cp38-cp38-win32.whl", hash = "sha256:538c6cdcdcdad310bbefd96d7850be1cd46e703079cc9e67d42a9ca776cdc8a8"}, + {file = "debugpy-1.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:22140bc02c66cda6053b6eb56dfe01bbe22a4447846581ba1dd6df2c9f97982d"}, + {file = "debugpy-1.8.6-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa"}, + {file = "debugpy-1.8.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881"}, + {file = "debugpy-1.8.6-cp39-cp39-win32.whl", hash = "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123"}, + {file = "debugpy-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51"}, + {file = "debugpy-1.8.6-py2.py3-none-any.whl", hash = "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f"}, + {file = "debugpy-1.8.6.zip", hash = "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a"}, ] [[package]] @@ -944,13 +944,13 @@ files = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -970,13 +970,13 @@ files = [ [[package]] name = "docutils" -version = "0.20.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -1067,53 +1067,59 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fonttools" -version = "4.53.1" +version = "4.54.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, - {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, - {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, - {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, - {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, - {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, - {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, - {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, - {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, - {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, - {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, - {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, - {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, - {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"}, + {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"}, + {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"}, + {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"}, + {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"}, + {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"}, + {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"}, + {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"}, + {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"}, + {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"}, + {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"}, + {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"}, + {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"}, + {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"}, + {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"}, + {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, + {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, ] [package.extras] @@ -1205,36 +1211,41 @@ tornado = ["tornado"] [[package]] name = "h5py" -version = "3.11.0" +version = "3.12.1" description = "Read and write HDF5 files from Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"}, - {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"}, - {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"}, - {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"}, - {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"}, - {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"}, - {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"}, - {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"}, - {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"}, - {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"}, - {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"}, - {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"}, - {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, + {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, + {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, + {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, + {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, + {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, + {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.19.3" [[package]] name = "hdf5plugin" @@ -3004,37 +3015,53 @@ files = [ [[package]] name = "pandas" -version = "2.2.2" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, - {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, - {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, - {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, - {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, - {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] @@ -3306,13 +3333,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.0.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.0.0-py2.py3-none-any.whl", hash = "sha256:0ca2341cf94ac1865350970951e54b1a50521e57b7b500403307aed4315a1234"}, + {file = "pre_commit-4.0.0.tar.gz", hash = "sha256:5d9807162cc5537940f94f266cbe2d716a75cfad0d78a317a92cac16287cfed6"}, ] [package.dependencies] @@ -3324,13 +3351,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prometheus-client" -version = "0.20.0" +version = "0.21.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, + {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, + {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, ] [package.extras] @@ -3338,13 +3365,13 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -3465,18 +3492,18 @@ files = [ [[package]] name = "pydantic" -version = "2.9.1" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, - {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.3" +pydantic-core = "2.23.4" typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} [package.extras] @@ -3485,100 +3512,100 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.3" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, - {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, - {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, - {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, - {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, - {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, - {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, - {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, - {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, - {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, - {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, - {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, - {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, - {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, - {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, - {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, - {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, - {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, - {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, - {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, - {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, - {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, - {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, - {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, - {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, - {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, - {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, - {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, - {file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"}, - {file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"}, - {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"}, - {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"}, - {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"}, - {file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"}, - {file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"}, - {file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"}, - {file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"}, - {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"}, - {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"}, - {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"}, - {file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"}, - {file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, - {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"}, - {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"}, - {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -3638,13 +3665,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.7.1" +version = "0.7.4" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.7.1-py3-none-any.whl", hash = "sha256:7f1a3eeef599b31a5571980cb2105074dc96c58dccb729703e1f4c4c46decbfd"}, - {file = "pynxtools-0.7.1.tar.gz", hash = "sha256:076b65cbe17ca822cb11d69841693a16c716fcb8c1452eba9bd27eec035242a1"}, + {file = "pynxtools-0.7.4-py3-none-any.whl", hash = "sha256:16a6a52ea8feb5847696bc6e474e31099840ae2fbcd1297bc5c37e756b7e6a62"}, + {file = "pynxtools-0.7.4.tar.gz", hash = "sha256:3e75a60cd92f0fea6eb9d456c135d62cf0f30cbdc54d73e71023d1db42920094"}, ] [package.dependencies] @@ -3820,25 +3847,29 @@ files = [ [[package]] name = "pywin32" -version = "306" +version = "307" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b"}, + {file = "pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d"}, + {file = "pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4"}, + {file = "pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75"}, + {file = "pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3"}, + {file = "pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398"}, + {file = "pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815"}, + {file = "pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347"}, + {file = "pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2"}, + {file = "pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5"}, + {file = "pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2"}, + {file = "pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a"}, + {file = "pywin32-307-cp37-cp37m-win32.whl", hash = "sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511"}, + {file = "pywin32-307-cp37-cp37m-win_amd64.whl", hash = "sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba"}, + {file = "pywin32-307-cp38-cp38-win32.whl", hash = "sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01"}, + {file = "pywin32-307-cp38-cp38-win_amd64.whl", hash = "sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b"}, + {file = "pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6"}, + {file = "pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87"}, ] [[package]] @@ -4135,18 +4166,19 @@ files = [ [[package]] name = "rich" -version = "13.8.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -4496,22 +4528,22 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", [[package]] name = "sphinx-rtd-theme" -version = "2.0.0" +version = "3.0.0" description = "Read the Docs theme for Sphinx" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, - {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, + {file = "sphinx_rtd_theme-3.0.0-py2.py3-none-any.whl", hash = "sha256:1ffe1539957775bfa0a7331370de7dc145b6eac705de23365dc55c5d94bb08e7"}, + {file = "sphinx_rtd_theme-3.0.0.tar.gz", hash = "sha256:905d67de03217fd3d76fbbdd992034ac8e77044ef8063a544dda1af74d409e08"}, ] [package.dependencies] -docutils = "<0.21" -sphinx = ">=5,<8" +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] +dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" @@ -4737,13 +4769,13 @@ test = ["pytest", "ruff"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -4759,13 +4791,13 @@ files = [ [[package]] name = "toolz" -version = "0.12.1" +version = "1.0.0" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, ] [[package]] @@ -4825,13 +4857,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-python-dateutil" -version = "2.9.0.20240906" +version = "2.9.0.20241003" description = "Typing stubs for python-dateutil" optional = true python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240906.tar.gz", hash = "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"}, - {file = "types_python_dateutil-2.9.0.20240906-py3-none-any.whl", hash = "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6"}, + {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, + {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, ] [[package]] @@ -4872,13 +4904,13 @@ files = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -4931,13 +4963,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.5" +version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies] @@ -5179,4 +5211,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "8c1003f86a5981c9514947b1ba8ddb44933aacd6ec695a57ea2d94f610dc8858" +content-hash = "611c9371c0929b1f155919c2962e356799564a027196c2b06c4f0da75d6b2314" From 715c42a4d3f343cfe2950ebc55ac5bc1390721f6 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 24 Jun 2024 23:40:01 +0200 Subject: [PATCH 205/300] update poetry, limit numpy --- poetry.lock | 12 ++++++------ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index f1b02cae..35b7b394 100644 --- a/poetry.lock +++ b/poetry.lock @@ -851,20 +851,20 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2023.12.0" +version = "2024.6.2" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2023.12.0-py3-none-any.whl", hash = "sha256:6cef2fa43815adc8fe895dd3ea473a311249ba0e15e1f7eb9b1b68302339d998"}, - {file = "dask-2023.12.0.tar.gz", hash = "sha256:3f687e647ced0d3f2cadbef113c730b6555b9499b62e3a220535438841001c91"}, + {file = "dask-2024.6.2-py3-none-any.whl", hash = "sha256:81b80ee015b2e057b93bb2d1bf13a866136e762e2b24bf54b6b621e8b86b7708"}, + {file = "dask-2024.6.2.tar.gz", hash = "sha256:d429d6b19e85fd1306ac37c188aaf99d03bbe69a6fe59d2b42882b2ac188686f"}, ] [package.dependencies] click = ">=8.1" cloudpickle = ">=1.5.0" fsspec = ">=2021.09.0" -importlib-metadata = ">=4.13.0" +importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} packaging = ">=20.0" partd = ">=1.2.0" pyyaml = ">=5.3.1" @@ -873,9 +873,9 @@ toolz = ">=0.10.0" [package.extras] array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] -dataframe = ["dask[array]", "pandas (>=1.3)"] +dataframe = ["dask-expr (>=1.1,<1.2)", "dask[array]", "pandas (>=1.3)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2023.12.0)"] +distributed = ["distributed (==2024.6.2)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 75e4892b..92a10c7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ keywords = ["sed", "mpes", "flash", "arpes"] license = "MIT" [tool.poetry.dependencies] -python = ">=3.9, <3.12.3, !=3.11.9" +python = ">=3.9, <3.13" bokeh = ">=2.4.2" -dask = ">=2021.12.0, <2023.12.1" +dask = ">=2021.12.0" fastdtw = ">=0.3.4" h5py = ">=3.6.0" ipympl = ">=0.9.1" From a70529a050201a07e43a92c7a74576bc85559fc8 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 11 Sep 2024 21:35:38 +0200 Subject: [PATCH 206/300] update lockfile --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 35b7b394..5a026897 100644 --- a/poetry.lock +++ b/poetry.lock @@ -851,13 +851,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2024.6.2" +version = "2024.8.0" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2024.6.2-py3-none-any.whl", hash = "sha256:81b80ee015b2e057b93bb2d1bf13a866136e762e2b24bf54b6b621e8b86b7708"}, - {file = "dask-2024.6.2.tar.gz", hash = "sha256:d429d6b19e85fd1306ac37c188aaf99d03bbe69a6fe59d2b42882b2ac188686f"}, + {file = "dask-2024.8.0-py3-none-any.whl", hash = "sha256:250ea3df30d4a25958290eec4f252850091c6cfaed82d098179c3b25bba18309"}, + {file = "dask-2024.8.0.tar.gz", hash = "sha256:f1fec39373d2f101bc045529ad4e9b30e34e6eb33b7aa0fa7073aec7b1bf9eee"}, ] [package.dependencies] @@ -866,16 +866,16 @@ cloudpickle = ">=1.5.0" fsspec = ">=2021.09.0" importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} packaging = ">=20.0" -partd = ">=1.2.0" +partd = ">=1.4.0" pyyaml = ">=5.3.1" toolz = ">=0.10.0" [package.extras] array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] -dataframe = ["dask-expr (>=1.1,<1.2)", "dask[array]", "pandas (>=1.3)"] +dataframe = ["dask-expr (>=1.1,<1.2)", "dask[array]", "pandas (>=2.0)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.6.2)"] +distributed = ["distributed (==2024.8.0)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] From daf5c2d06d56983539c8c1fc2608eadb50234868 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 00:05:20 +0200 Subject: [PATCH 207/300] limit dask version to <2024.8 due to bug where computation takes forwever in certain cases --- poetry.lock | 907 ++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 474 insertions(+), 435 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5a026897..9d92d6f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -185,13 +185,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] [[package]] name = "asteval" -version = "1.0.4" +version = "1.0.5" description = "Safe, minimalistic evaluator of python expression using ast module" optional = false python-versions = ">=3.8" files = [ - {file = "asteval-1.0.4-py3-none-any.whl", hash = "sha256:7a88bfd0dd1eabdf20bb4995904df742cecf876f7f9e700f22231abf4e34d50c"}, - {file = "asteval-1.0.4.tar.gz", hash = "sha256:15e63bd01fce65aded51357f7e1debc6f46100d777c372af11c27c07cb740074"}, + {file = "asteval-1.0.5-py3-none-any.whl", hash = "sha256:082b95312578affc8a6d982f7d92b7ac5de05634985c87e7eedd3188d31149fa"}, + {file = "asteval-1.0.5.tar.gz", hash = "sha256:bac3c8dd6d2b789e959cfec9bb296fb8338eec066feae618c462132701fbc665"}, ] [package.extras] @@ -254,13 +254,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.9.16.0.32.21" +version = "0.2024.10.7.0.32.46" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" files = [ - {file = "astropy_iers_data-0.2024.9.16.0.32.21-py3-none-any.whl", hash = "sha256:adf111e1b596470c4437fa44cf767e56f6d4bc2e93068871fd0b30c73476d430"}, - {file = "astropy_iers_data-0.2024.9.16.0.32.21.tar.gz", hash = "sha256:2ff6fe868a623e616953a432698b05dd6adac9683d21ac780bfbb94e78f7c344"}, + {file = "astropy_iers_data-0.2024.10.7.0.32.46-py3-none-any.whl", hash = "sha256:4b5ce3e3c8ac17632a28a35f4b847d8a8db084509f5f0db3238140dbb5149eae"}, + {file = "astropy_iers_data-0.2024.10.7.0.32.46.tar.gz", hash = "sha256:349ff09294f03112d9391a5ad660135bf2fcef7de411a52c20bcf8598fb029d1"}, ] [package.extras] @@ -482,101 +482,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -612,13 +627,13 @@ test = ["pytest"] [[package]] name = "cloudpickle" -version = "3.0.0" +version = "3.1.0" description = "Pickler class to extend the standard pickle.Pickler functionality" optional = false python-versions = ">=3.8" files = [ - {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"}, - {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"}, + {file = "cloudpickle-3.1.0-py3-none-any.whl", hash = "sha256:fe11acda67f61aaaec473e3afe030feb131d78a43461b718185363384f1ba12e"}, + {file = "cloudpickle-3.1.0.tar.gz", hash = "sha256:81a929b6e3c7335c863c771d673d105f02efdb89dfaba0c90495d1c64796601b"}, ] [[package]] @@ -749,83 +764,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.2" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, + {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, + {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, + {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, + {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, + {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, + {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, + {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, + {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, + {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, + {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, + {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, + {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, + {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, + {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, ] [package.dependencies] @@ -851,13 +856,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2024.8.0" +version = "2024.7.1" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2024.8.0-py3-none-any.whl", hash = "sha256:250ea3df30d4a25958290eec4f252850091c6cfaed82d098179c3b25bba18309"}, - {file = "dask-2024.8.0.tar.gz", hash = "sha256:f1fec39373d2f101bc045529ad4e9b30e34e6eb33b7aa0fa7073aec7b1bf9eee"}, + {file = "dask-2024.7.1-py3-none-any.whl", hash = "sha256:dd046840050376c317de90629db5c6197adda820176cf3e2df10c3219d11951f"}, + {file = "dask-2024.7.1.tar.gz", hash = "sha256:dbaef2d50efee841a9d981a218cfeb50392fc9a95e0403b6d680450e4f50d531"}, ] [package.dependencies] @@ -875,38 +880,42 @@ array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] dataframe = ["dask-expr (>=1.1,<1.2)", "dask[array]", "pandas (>=2.0)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.8.0)"] +distributed = ["distributed (==2024.7.1)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] name = "debugpy" -version = "1.8.5" +version = "1.8.7" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, - {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, - {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, - {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, - {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, - {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, - {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, - {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, - {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, - {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, - {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, - {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, - {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, - {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, - {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, - {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, - {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, - {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, - {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, - {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, - {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, - {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, + {file = "debugpy-1.8.7-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95fe04a573b8b22896c404365e03f4eda0ce0ba135b7667a1e57bd079793b96b"}, + {file = "debugpy-1.8.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:628a11f4b295ffb4141d8242a9bb52b77ad4a63a2ad19217a93be0f77f2c28c9"}, + {file = "debugpy-1.8.7-cp310-cp310-win32.whl", hash = "sha256:85ce9c1d0eebf622f86cc68618ad64bf66c4fc3197d88f74bb695a416837dd55"}, + {file = "debugpy-1.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:29e1571c276d643757ea126d014abda081eb5ea4c851628b33de0c2b6245b037"}, + {file = "debugpy-1.8.7-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:caf528ff9e7308b74a1749c183d6808ffbedbb9fb6af78b033c28974d9b8831f"}, + {file = "debugpy-1.8.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba1d078cf2e1e0b8402e6bda528bf8fda7ccd158c3dba6c012b7897747c41a0"}, + {file = "debugpy-1.8.7-cp311-cp311-win32.whl", hash = "sha256:171899588bcd412151e593bd40d9907133a7622cd6ecdbdb75f89d1551df13c2"}, + {file = "debugpy-1.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:6e1c4ffb0c79f66e89dfd97944f335880f0d50ad29525dc792785384923e2211"}, + {file = "debugpy-1.8.7-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:4d27d842311353ede0ad572600c62e4bcd74f458ee01ab0dd3a1a4457e7e3706"}, + {file = "debugpy-1.8.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2"}, + {file = "debugpy-1.8.7-cp312-cp312-win32.whl", hash = "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca"}, + {file = "debugpy-1.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39"}, + {file = "debugpy-1.8.7-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40"}, + {file = "debugpy-1.8.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7"}, + {file = "debugpy-1.8.7-cp313-cp313-win32.whl", hash = "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba"}, + {file = "debugpy-1.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa"}, + {file = "debugpy-1.8.7-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:4b908291a1d051ef3331484de8e959ef3e66f12b5e610c203b5b75d2725613a7"}, + {file = "debugpy-1.8.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da8df5b89a41f1fd31503b179d0a84a5fdb752dddd5b5388dbd1ae23cda31ce9"}, + {file = "debugpy-1.8.7-cp38-cp38-win32.whl", hash = "sha256:b12515e04720e9e5c2216cc7086d0edadf25d7ab7e3564ec8b4521cf111b4f8c"}, + {file = "debugpy-1.8.7-cp38-cp38-win_amd64.whl", hash = "sha256:93176e7672551cb5281577cdb62c63aadc87ec036f0c6a486f0ded337c504596"}, + {file = "debugpy-1.8.7-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:90d93e4f2db442f8222dec5ec55ccfc8005821028982f1968ebf551d32b28907"}, + {file = "debugpy-1.8.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6db2a370e2700557a976eaadb16243ec9c91bd46f1b3bb15376d7aaa7632c81"}, + {file = "debugpy-1.8.7-cp39-cp39-win32.whl", hash = "sha256:a6cf2510740e0c0b4a40330640e4b454f928c7b99b0c9dbf48b11efba08a8cda"}, + {file = "debugpy-1.8.7-cp39-cp39-win_amd64.whl", hash = "sha256:6a9d9d6d31846d8e34f52987ee0f1a904c7baa4912bf4843ab39dadf9b8f3e0d"}, + {file = "debugpy-1.8.7-py2.py3-none-any.whl", hash = "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae"}, + {file = "debugpy-1.8.7.zip", hash = "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e"}, ] [[package]] @@ -933,13 +942,13 @@ files = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -948,24 +957,24 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "docutils" -version = "0.20.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -1056,53 +1065,59 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fonttools" -version = "4.53.1" +version = "4.54.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, - {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, - {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, - {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, - {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, - {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, - {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, - {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, - {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, - {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, - {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, - {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, - {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, - {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"}, + {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"}, + {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"}, + {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"}, + {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"}, + {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"}, + {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"}, + {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"}, + {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"}, + {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"}, + {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"}, + {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"}, + {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"}, + {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"}, + {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"}, + {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, + {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, ] [package.extras] @@ -1194,36 +1209,41 @@ tornado = ["tornado"] [[package]] name = "h5py" -version = "3.11.0" +version = "3.12.1" description = "Read and write HDF5 files from Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"}, - {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"}, - {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"}, - {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"}, - {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"}, - {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"}, - {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"}, - {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"}, - {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"}, - {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"}, - {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"}, - {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"}, - {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, + {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, + {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, + {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, + {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, + {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, + {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.19.3" [[package]] name = "hdf5plugin" @@ -1615,13 +1635,13 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.12.1" +version = "2024.10.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, ] [package.dependencies] @@ -2311,71 +2331,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, + {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, ] [[package]] @@ -2993,40 +3014,53 @@ files = [ [[package]] name = "pandas" -version = "2.2.2" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, - {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, - {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, - {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, - {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, - {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] @@ -3298,13 +3332,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, + {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, ] [package.dependencies] @@ -3316,13 +3350,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prometheus-client" -version = "0.20.0" +version = "0.21.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, + {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, + {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, ] [package.extras] @@ -3330,13 +3364,13 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -3484,13 +3518,13 @@ test = ["pytest", "pytest-doctestplus (>=0.7)"] [[package]] name = "pyfakefs" -version = "5.6.0" +version = "5.7.0" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ - {file = "pyfakefs-5.6.0-py3-none-any.whl", hash = "sha256:1a45bba8615323ec29d65929d32dc66d7b59a1e60a02109950440edb0486c539"}, - {file = "pyfakefs-5.6.0.tar.gz", hash = "sha256:7a549b32865aa97d8ba6538285a93816941d9b7359be2954ac60ec36b277e879"}, + {file = "pyfakefs-5.7.0-py3-none-any.whl", hash = "sha256:0ebf388a79fd937a31e761bf75b6caf7eae8b5d143493a00b18d5cd590d78356"}, + {file = "pyfakefs-5.7.0.tar.gz", hash = "sha256:9cbf0e4f22891e7db94bd4e764a95e91a9beda371cc11fdff3c537d88987696a"}, ] [[package]] @@ -3509,13 +3543,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.7.1" +version = "0.7.4" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.7.1-py3-none-any.whl", hash = "sha256:7f1a3eeef599b31a5571980cb2105074dc96c58dccb729703e1f4c4c46decbfd"}, - {file = "pynxtools-0.7.1.tar.gz", hash = "sha256:076b65cbe17ca822cb11d69841693a16c716fcb8c1452eba9bd27eec035242a1"}, + {file = "pynxtools-0.7.4-py3-none-any.whl", hash = "sha256:16a6a52ea8feb5847696bc6e474e31099840ae2fbcd1297bc5c37e756b7e6a62"}, + {file = "pynxtools-0.7.4.tar.gz", hash = "sha256:3e75a60cd92f0fea6eb9d456c135d62cf0f30cbdc54d73e71023d1db42920094"}, ] [package.dependencies] @@ -3691,25 +3725,29 @@ files = [ [[package]] name = "pywin32" -version = "306" +version = "307" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b"}, + {file = "pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d"}, + {file = "pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4"}, + {file = "pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75"}, + {file = "pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3"}, + {file = "pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398"}, + {file = "pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815"}, + {file = "pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347"}, + {file = "pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2"}, + {file = "pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5"}, + {file = "pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2"}, + {file = "pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a"}, + {file = "pywin32-307-cp37-cp37m-win32.whl", hash = "sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511"}, + {file = "pywin32-307-cp37-cp37m-win_amd64.whl", hash = "sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba"}, + {file = "pywin32-307-cp38-cp38-win32.whl", hash = "sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01"}, + {file = "pywin32-307-cp38-cp38-win_amd64.whl", hash = "sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b"}, + {file = "pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6"}, + {file = "pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87"}, ] [[package]] @@ -4006,18 +4044,19 @@ files = [ [[package]] name = "rich" -version = "13.8.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -4367,22 +4406,22 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", [[package]] name = "sphinx-rtd-theme" -version = "2.0.0" +version = "3.0.1" description = "Read the Docs theme for Sphinx" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, - {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, + {file = "sphinx_rtd_theme-3.0.1-py2.py3-none-any.whl", hash = "sha256:921c0ece75e90633ee876bd7b148cfaad136b481907ad154ac3669b6fc957916"}, + {file = "sphinx_rtd_theme-3.0.1.tar.gz", hash = "sha256:a4c5745d1b06dfcb80b7704fe532eb765b44065a8fad9851e4258c8804140703"}, ] [package.dependencies] -docutils = "<0.21" -sphinx = ">=5,<8" +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] +dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" @@ -4608,13 +4647,13 @@ test = ["pytest", "ruff"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -4630,13 +4669,13 @@ files = [ [[package]] name = "toolz" -version = "0.12.1" +version = "1.0.0" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, ] [[package]] @@ -4696,13 +4735,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-python-dateutil" -version = "2.9.0.20240906" +version = "2.9.0.20241003" description = "Typing stubs for python-dateutil" optional = true python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240906.tar.gz", hash = "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"}, - {file = "types_python_dateutil-2.9.0.20240906-py3-none-any.whl", hash = "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6"}, + {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, + {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, ] [[package]] @@ -4743,13 +4782,13 @@ files = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -4802,13 +4841,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.5" +version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies] @@ -5049,5 +5088,5 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" -python-versions = ">=3.9, <3.12.3, !=3.11.9" -content-hash = "8c1003f86a5981c9514947b1ba8ddb44933aacd6ec695a57ea2d94f610dc8858" +python-versions = ">=3.9, <3.13" +content-hash = "a98574d0e71b520a728922e9494f6948468f589f1fd023996911dcc2eddba7fc" diff --git a/pyproject.toml b/pyproject.toml index 92a10c7e..6e525864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = "MIT" [tool.poetry.dependencies] python = ">=3.9, <3.13" bokeh = ">=2.4.2" -dask = ">=2021.12.0" +dask = ">=2021.12.0, <2024.8" fastdtw = ">=0.3.4" h5py = ">=3.6.0" ipympl = ">=0.9.1" From 1aa4a83ec7ae94fa615c3c5e733536419a905bf7 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 00:06:07 +0200 Subject: [PATCH 208/300] disable dask query-planning as workaround for bugs in dask-expr --- sed/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sed/__init__.py b/sed/__init__.py index 50850279..3d03ef82 100644 --- a/sed/__init__.py +++ b/sed/__init__.py @@ -1,7 +1,11 @@ """sed module easy access APIs.""" import importlib.metadata -from .core.processor import SedProcessor +import dask + +dask.config.set({"dataframe.query-planning": False}) + +from .core.processor import SedProcessor # noqa: E402 __version__ = importlib.metadata.version("sed-processor") __all__ = ["SedProcessor"] From 41f05ef209acd32fa9be284fe0295251fc3c090b Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 00:17:46 +0200 Subject: [PATCH 209/300] remove limits on python versions for tests --- .github/workflows/testing_multiversion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index 0c016113..f0efc943 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -13,7 +13,7 @@ jobs: # Using matrix strategy strategy: matrix: - python-version: ["3.9", "3.10", "3.11.8", "3.12.2"] + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest steps: # Check out repo and set up Python From 1146b4f94d4c5a8545807ca5f6115bbb961883d8 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 21:59:05 +0200 Subject: [PATCH 210/300] fix tests for calibrators --- .cspell/custom-dictionary.txt | 1 + sed/calibrator/delay.py | 34 +++++++++--------- sed/calibrator/energy.py | 64 +++++++++++++++++---------------- sed/config/config_model.py | 49 +++++++++++++++++-------- sed/core/config.py | 2 +- tests/calibrator/test_delay.py | 6 ++-- tests/calibrator/test_energy.py | 58 ++++++++++++++++-------------- tests/test_processor.py | 2 +- 8 files changed, 124 insertions(+), 92 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 406f9f0f..f4cc3e5c 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -290,6 +290,7 @@ ptargs pullrequest pval pyarrow +pydantic pyenv pygments pynxtools diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index a15edd15..13c4436f 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -288,6 +288,7 @@ def add_offsets( offsets["creation_date"] = datetime.now().timestamp() # column-based offsets if columns is not None: + offsets["columns"] = {} if weights is None: weights = 1 if isinstance(weights, (int, float, np.integer, np.floating)): @@ -314,7 +315,7 @@ def add_offsets( # store in offsets dictionary for col, weight, pmean, red in zip(columns, weights, preserve_mean, reductions): - offsets[col] = { + offsets["columns"][col] = { "weight": weight, "preserve_mean": pmean, "reduction": red, @@ -359,21 +360,22 @@ def add_offsets( f"Invalid value for flip_delay_axis in config: {flip_delay_axis}.", ) log_str += f"\n Flip delay axis: {flip_delay_axis}" - else: - columns.append(k) - try: - weight = v["weight"] - except KeyError: - weight = 1 - weights.append(weight) - pm = v.get("preserve_mean", False) - preserve_mean.append(pm) - red = v.get("reduction", None) - reductions.append(red) - log_str += ( - f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " - f"Reductions: {red}." - ) + elif k == "columns": + for k2, v2 in offsets["columns"].items(): + columns.append(k2) + try: + weight = v2["weight"] + except KeyError: + weight = 1 + weights.append(weight) + pm = v2.get("preserve_mean", False) + preserve_mean.append(pm) + red = v2.get("reduction", None) + reductions.append(red) + log_str += ( + f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"Reductions: {red}." + ) if not suppress_output: logger.info(log_str) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 83429ed2..72cfa801 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -915,17 +915,17 @@ def append_energy_axis( df[energy_column] = df[energy_column] + scale_sign * bias_voltage if not suppress_output: logger.debug(f"Shifted energy column by constant bias value: {bias_voltage}.") - elif self._config["dataframe"]["bias_column"] in df.columns: + elif self._config["dataframe"]["columns"]["bias"] in df.columns: df = dfops.offset_by_other_columns( df=df, target_column=energy_column, - offset_columns=self._config["dataframe"]["bias_column"], + offset_columns=self._config["dataframe"]["columns"]["bias"], weights=scale_sign, ) if not suppress_output: logger.debug( "Shifted energy column by bias column: " - f"{self._config['dataframe']['bias_column']}.", + f"{self._config['dataframe']['columns']['bias']}.", ) else: logger.warning( @@ -1595,6 +1595,7 @@ def add_offsets( offsets["creation_date"] = datetime.now().timestamp() # column-based offsets if columns is not None: + offsets["columns"] = {} if isinstance(columns, str): columns = [columns] @@ -1623,7 +1624,7 @@ def add_offsets( # store in offsets dictionary for col, weight, pmean, red in zip(columns, weights, preserve_mean, reductions): - offsets[col] = { + offsets["columns"][col] = { "weight": weight, "preserve_mean": pmean, "reduction": red, @@ -1652,35 +1653,38 @@ def add_offsets( for k, v in offsets.items(): if k == "creation_date": continue - if k == "constant": + elif k == "constant": # flip sign if binding energy scale constant = v * scale_sign log_str += f"\n Constant: {constant}" - else: - columns.append(k) - try: - weight = v["weight"] - except KeyError: - weight = 1 - if not isinstance(weight, (int, float, np.integer, np.floating)): - raise TypeError(f"Invalid type for weight of column {k}: {type(weight)}") - # flip sign if binding energy scale - weight = weight * scale_sign - weights.append(weight) - pm = v.get("preserve_mean", False) - if str(pm).lower() in ["false", "0", "no"]: - pm = False - elif str(pm).lower() in ["true", "1", "yes"]: - pm = True - preserve_mean.append(pm) - red = v.get("reduction", None) - if str(red).lower() in ["none", "null"]: - red = None - reductions.append(red) - log_str += ( - f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " - f"Reductions: {red}." - ) + elif k == "columns": + for k2, v2 in offsets["columns"].items(): + columns.append(k2) + try: + weight = v2["weight"] + except KeyError: + weight = 1 + if not isinstance(weight, (int, float, np.integer, np.floating)): + raise TypeError( + f"Invalid type for weight of column {k}: {type(weight)}", + ) + # flip sign if binding energy scale + weight = weight * scale_sign + weights.append(weight) + pm = v2.get("preserve_mean", False) + if str(pm).lower() in ["false", "0", "no"]: + pm = False + elif str(pm).lower() in ["true", "1", "yes"]: + pm = True + preserve_mean.append(pm) + red = v2.get("reduction", None) + if str(red).lower() in ["none", "null"]: + red = None + reductions.append(red) + log_str += ( + f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"Reductions: {red}." + ) if not suppress_output: logger.info(log_str) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 2356d4dd..16095adf 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -1,6 +1,5 @@ """Pydantic model to validate the config for SED package.""" from collections.abc import Sequence -from datetime import datetime from typing import Literal from typing import Optional from typing import Union @@ -99,7 +98,7 @@ class DataframeModel(BaseModel): ubid_offset: Optional[int] = None split_sector_id_from_dld_time: Optional[bool] = None sector_id_reserved_bits: Optional[int] = None - sector_delays: Optional[Sequence[int]] = None + sector_delays: Optional[Sequence[float]] = None # write validator for model so that x_column gets converted to columns: x @@ -142,22 +141,42 @@ class EnergyModel(BaseModel): x_width: Sequence[int] y_width: Sequence[int] color_clip: int + bias_key: Optional[str] = None class EnergyCalibrationModel(BaseModel): - d: float - t0: float - E0: float + creation_date: Optional[float] = None + d: Optional[float] = None + t0: Optional[float] = None + E0: Optional[float] = None energy_scale: str calibration: Optional[EnergyCalibrationModel] = None + class EnergyOffsets(BaseModel): + creation_date: Optional[float] = None + constant: Optional[float] = None + + ## This seems rather complicated way to define offsets, + # inconsistent in how args vs config are for add_offsets + class OffsetColumn(BaseModel): + weight: float + preserve_mean: bool + reduction: Optional[str] = None + + columns: Optional[dict[str, OffsetColumn]] = None + + offsets: Optional[EnergyOffsets] = None + class EnergyCorrectionModel(BaseModel): + creation_date: Optional[float] = None correction_type: str amplitude: float center: Sequence[float] - gamma: float - sigma: float - diameter: float + gamma: Optional[float] = None + sigma: Optional[float] = None + diameter: Optional[float] = None + sigma2: Optional[float] = None + amplitude2: Optional[float] = None correction: Optional[EnergyCorrectionModel] = None @@ -173,6 +192,7 @@ class MomentumModel(BaseModel): sigma_radius: int class MomentumCalibrationModel(BaseModel): + creation_date: Optional[float] = None kx_scale: float ky_scale: float x_center: float @@ -185,6 +205,7 @@ class MomentumCalibrationModel(BaseModel): calibration: Optional[MomentumCalibrationModel] = None class MomentumCorrectionModel(BaseModel): + creation_date: Optional[float] = None feature_points: Sequence[Sequence[float]] rotation_symmetry: int include_center: bool @@ -202,18 +223,18 @@ class DelayModel(BaseModel): p3_key: Optional[str] = None t0_key: Optional[str] = None - class Calibration(BaseModel): - creation_date: datetime + class DelayCalibration(BaseModel): + creation_date: Optional[float] = None adc_range: Sequence[int] delay_range: Sequence[float] time0: float delay_range_mm: Sequence[float] datafile: FilePath # .h5 extension in filepath - calibration: Optional[Calibration] = None + calibration: Optional[DelayCalibration] = None - class Offsets(BaseModel): - creation_date: Optional[datetime] = None + class DelayOffsets(BaseModel): + creation_date: Optional[float] = None constant: Optional[float] = None flip_delay_axis: Optional[bool] = False @@ -226,7 +247,7 @@ class OffsetColumn(BaseModel): columns: Optional[dict[str, OffsetColumn]] = None - offsets: Optional[Offsets] = None + offsets: Optional[DelayOffsets] = None class MetadataModel(BaseModel): diff --git a/sed/core/config.py b/sed/core/config.py index df44cfd1..ff11bbd7 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -148,7 +148,7 @@ def parse_config( return config_dict # Run the config through the ConfigModel to ensure it is valid config_model = ConfigModel(**config_dict) - return config_model.model_dump() + return config_model.model_dump(exclude_unset=True, exclude_none=True, exclude_defaults=True) def load_config(config_path: str) -> dict: diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index a81542e2..5ec9fc2c 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -167,7 +167,7 @@ def test_add_offset_from_config(df=test_dataframe) -> None: dc = DelayCalibrator(config=config) df, _ = dc.add_offsets(df.copy()) assert "delay" in df.columns - assert "bam" in dc.offsets.keys() + assert "bam" in dc.offsets["columns"].keys() np.testing.assert_allclose(expected, df["delay"]) @@ -189,7 +189,7 @@ def test_add_offset_from_args(df=test_dataframe) -> None: columns="bam", ) assert "delay" in df.columns - assert "bam" in dc.offsets.keys() + assert "bam" in dc.offsets["columns"].keys() expected = -np.array( delay_stage_vals + bam_vals * 1 + 1, ) @@ -215,5 +215,5 @@ def test_add_offset_from_dict(df=test_dataframe) -> None: dc = DelayCalibrator(config=config) df, _ = dc.add_offsets(df.copy(), offsets=offsets) assert "delay" in df.columns - assert "bam" in dc.offsets.keys() + assert "bam" in dc.offsets["columns"].keys() np.testing.assert_allclose(expected, df["delay"]) diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 32dd1a4b..9ec24229 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -210,7 +210,7 @@ def test_calibrate_append(energy_scale: str, calibration_method: str) -> None: method=calibration_method, ) df, metadata = ec.append_energy_axis(df) - assert config["dataframe"]["energy_column"] in df.columns + assert config["dataframe"]["columns"]["energy"] in df.columns axis = calibdict["axis"] diff = np.diff(axis) if energy_scale == "kinetic": @@ -256,7 +256,7 @@ def test_append_energy_axis_from_dict_kwds(calib_type: str, calib_dict: dict) -> df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) ec = EnergyCalibrator(config=config, loader=loader) df, metadata = ec.append_energy_axis(df, calibration=calib_dict) - assert config["dataframe"]["energy_column"] in df.columns + assert config["dataframe"]["columns"]["energy"] in df.columns for key in calib_dict: np.testing.assert_equal(metadata["calibration"][key], calib_dict[key]) @@ -266,7 +266,7 @@ def test_append_energy_axis_from_dict_kwds(calib_type: str, calib_dict: dict) -> df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) ec = EnergyCalibrator(config=config, loader=loader) df, metadata = ec.append_energy_axis(df, **calib_dict) - assert config["dataframe"]["energy_column"] in df.columns + assert config["dataframe"]["columns"]["energy"] in df.columns for key in calib_dict: np.testing.assert_equal(metadata["calibration"][key], calib_dict[key]) @@ -307,14 +307,14 @@ def test_append_tof_ns_axis() -> None: df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) ec = EnergyCalibrator(config=config, loader=loader) df, _ = ec.append_tof_ns_axis(df, binwidth=2e-9, binning=2) - assert config["dataframe"]["tof_ns_column"] in df.columns + assert config["dataframe"]["columns"]["tof_ns"] in df.columns np.testing.assert_allclose(df[ec.tof_column], df[ec.tof_ns_column] / 4) # from config df, _, _ = loader.read_dataframe(folders=df_folder, collect_metadata=False) ec = EnergyCalibrator(config=config, loader=loader) df, _ = ec.append_tof_ns_axis(df) - assert config["dataframe"]["tof_ns_column"] in df.columns + assert config["dataframe"]["columns"]["tof_ns"] in df.columns np.testing.assert_allclose(df[ec.tof_column], df[ec.tof_ns_column] / 2) # illegal keywords: @@ -390,7 +390,7 @@ def test_energy_correction(correction_type: str, correction_kwd: dict) -> None: **correction_kwd, ) df, metadata = ec.apply_energy_correction(sample_df) - t = df[config["dataframe"]["corrected_tof_column"]] + t = df[config["dataframe"]["columns"]["corrected_tof"]] assert t[0] == t[2] assert t[0] < t[1] assert t[3] == t[5] @@ -426,7 +426,7 @@ def test_energy_correction(correction_type: str, correction_kwd: dict) -> None: **correction, ) df, metadata = ec.apply_energy_correction(sample_df) - t = df[config["dataframe"]["corrected_tof_column"]] + t = df[config["dataframe"]["columns"]["corrected_tof"]] assert t[0] == t[2] assert t[0] < t[1] assert t[3] == t[5] @@ -514,7 +514,7 @@ def test_energy_correction_from_dict_kwds(correction_type: str, correction_kwd: sample_df, correction=correction_dict, ) - t = df[config["dataframe"]["corrected_tof_column"]] + t = df[config["dataframe"]["columns"]["corrected_tof"]] assert t[0] == t[2] assert t[0] < t[1] assert t[3] == t[5] @@ -534,7 +534,7 @@ def test_energy_correction_from_dict_kwds(correction_type: str, correction_kwd: loader=get_loader("mpes", config=config), ) df, metadata = ec.apply_energy_correction(sample_df, **correction_dict) - t = df[config["dataframe"]["corrected_tof_column"]] + t = df[config["dataframe"]["columns"]["corrected_tof"]] assert t[0] == t[2] assert t[0] < t[1] assert t[3] == t[5] @@ -585,7 +585,7 @@ def test_apply_energy_correction_raises(correction_type: str) -> None: sample_df, correction=correction_dict, ) - assert config["dataframe"]["corrected_tof_column"] in df.columns + assert config["dataframe"]["columns"]["corrected_tof"] in df.columns @pytest.mark.parametrize( @@ -603,12 +603,14 @@ def test_add_offsets_functionality(energy_scale: str) -> None: }, "offsets": { "constant": 1, - "off1": { - "weight": 1, - "preserve_mean": True, + "columns": { + "off1": { + "weight": 1, + "preserve_mean": True, + }, + "off2": {"weight": -1, "preserve_mean": False}, + "off3": {"weight": 1, "preserve_mean": False, "reduction": "mean"}, }, - "off2": {"weight": -1, "preserve_mean": False}, - "off3": {"weight": 1, "preserve_mean": False, "reduction": "mean"}, }, }, }, @@ -684,9 +686,11 @@ def test_add_offset_raises() -> None: }, "offsets": { "constant": 1, - "off1": {"weight": -1, "preserve_mean": True}, - "off2": {"weight": -1, "preserve_mean": False}, - "off3": {"weight": 1, "preserve_mean": False, "reduction": "mean"}, + "columns": { + "off1": {"weight": -1, "preserve_mean": True}, + "off2": {"weight": -1, "preserve_mean": False}, + "off3": {"weight": 1, "preserve_mean": False, "reduction": "mean"}, + }, }, }, } @@ -719,17 +723,15 @@ def test_add_offset_raises() -> None: # invalid sign with pytest.raises(TypeError): - cfg = deepcopy(cfg_dict) - cfg["energy"]["offsets"]["off1"]["weight"] = "wrong_type" - config = parse_config(config=cfg, folder_config={}, user_config={}, system_config={}) + config = parse_config(config=cfg_dict, folder_config={}, user_config={}, system_config={}) + config["energy"]["offsets"]["columns"]["off1"]["weight"] = "wrong_type" ec = EnergyCalibrator(config=config, loader=get_loader("flash", config=config)) _ = ec.add_offsets(t_df) # invalid constant with pytest.raises(TypeError): - cfg = deepcopy(cfg_dict) - cfg["energy"]["offsets"]["constant"] = "wrong_type" - config = parse_config(config=cfg, folder_config={}, user_config={}, system_config={}) + config = parse_config(config=cfg_dict, folder_config={}, user_config={}, system_config={}) + config["energy"]["offsets"]["constant"] = "wrong_type" ec = EnergyCalibrator(config=config, loader=get_loader("flash", config=config)) _ = ec.add_offsets(t_df) @@ -738,8 +740,10 @@ def test_align_dld_sectors() -> None: """test functionality and error handling of align_dld_sectors""" cfg_dict: dict[str, Any] = { "dataframe": { - "tof_column": "dldTimeSteps", - "sector_id_column": "dldSectorId", + "columns": { + "tof": "dldTimeSteps", + "sector_id": "dldSectorId", + }, "sector_delays": [-0.35, -0.25, -0.15, -0.05, 0.05, 0.15, 0.25, 0.35], }, } @@ -767,7 +771,7 @@ def test_align_dld_sectors() -> None: t_df = dask.dataframe.from_pandas(df.copy(), npartitions=2) res, meta = ec.align_dld_sectors( t_df, - tof_column=cfg_dict["dataframe"]["tof_column"], + tof_column=cfg_dict["dataframe"]["columns"]["tof"], sector_delays=cfg_dict["dataframe"]["sector_delays"], sector_id_column="dldSectorId", ) diff --git a/tests/test_processor.py b/tests/test_processor.py index 21e4df86..7070dead 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -719,7 +719,7 @@ def test_append_tof_ns_axis() -> None: verbose=True, ) processor.append_tof_ns_axis() - assert processor.config["dataframe"]["tof_ns_column"] in processor.dataframe + assert processor.config["dataframe"]["columns"]["tof_ns"] in processor.dataframe def test_delay_calibration_workflow() -> None: From a1a9b27a4e5698f8cce39db9cfc1c592b9c60647 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 23:08:34 +0200 Subject: [PATCH 211/300] make model fail on extra parameters --- sed/config/config_model.py | 48 ++++++++++++++++++++++++++++++++++++-- sed/config/default.yaml | 2 -- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 16095adf..d2cbcd96 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -20,11 +20,15 @@ class Paths(BaseModel): + model_config = ConfigDict(extra="forbid") + raw: DirectoryPath processed: Union[DirectoryPath, NewPath] class CoreModel(BaseModel): + model_config = ConfigDict(extra="forbid") + loader: str verbose: Optional[bool] = None paths: Optional[Paths] = None @@ -32,6 +36,7 @@ class CoreModel(BaseModel): year: Optional[int] = None beamtime_id: Optional[int] = None instrument: Optional[str] = None + beamline: Optional[str] = None # TODO: move copy tool to a separate model use_copy_tool: Optional[bool] = None copy_tool_source: Optional[DirectoryPath] = None @@ -49,6 +54,8 @@ def validate_loader(cls, v: str) -> str: class ColumnsModel(BaseModel): + model_config = ConfigDict(extra="forbid") + x: str y: str tof: str @@ -69,6 +76,8 @@ class ColumnsModel(BaseModel): class ChannelModel(BaseModel): + model_config = ConfigDict(extra="forbid") + format: Literal["per_train", "per_electron", "per_pulse", "per_file"] dataset_key: str index_key: Optional[str] = None @@ -76,6 +85,8 @@ class ChannelModel(BaseModel): dtype: Optional[str] = None class subChannel(BaseModel): + model_config = ConfigDict(extra="forbid") + slice: int dtype: Optional[str] = None @@ -83,6 +94,8 @@ class subChannel(BaseModel): class DataframeModel(BaseModel): + model_config = ConfigDict(extra="forbid") + columns: ColumnsModel units: Optional[dict[str, str]] = None channels: Optional[dict[str, ChannelModel]] = None @@ -99,11 +112,14 @@ class DataframeModel(BaseModel): split_sector_id_from_dld_time: Optional[bool] = None sector_id_reserved_bits: Optional[int] = None sector_delays: Optional[Sequence[float]] = None + daq: Optional[str] = None # write validator for model so that x_column gets converted to columns: x class BinningModel(BaseModel): + model_config = ConfigDict(extra="forbid") + hist_mode: str mode: str pbar: bool @@ -112,6 +128,8 @@ class BinningModel(BaseModel): class HistogramModel(BaseModel): + model_config = ConfigDict(extra="forbid") + bins: Sequence[int] axes: Sequence[str] ranges: Sequence[tuple[float, float]] @@ -121,12 +139,16 @@ class StaticModel(BaseModel): """Static configuration settings that shouldn't be changed by users.""" # flash specific settings + model_config = ConfigDict(extra="forbid") + stream_name_prefixes: Optional[dict] = None stream_name_postfixes: Optional[dict] = None beamtime_dir: Optional[dict] = None class EnergyModel(BaseModel): + model_config = ConfigDict(extra="forbid") + bins: int ranges: Sequence[int] normalize: bool @@ -144,6 +166,8 @@ class EnergyModel(BaseModel): bias_key: Optional[str] = None class EnergyCalibrationModel(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None d: Optional[float] = None t0: Optional[float] = None @@ -153,6 +177,8 @@ class EnergyCalibrationModel(BaseModel): calibration: Optional[EnergyCalibrationModel] = None class EnergyOffsets(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None constant: Optional[float] = None @@ -168,6 +194,8 @@ class OffsetColumn(BaseModel): offsets: Optional[EnergyOffsets] = None class EnergyCorrectionModel(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None correction_type: str amplitude: float @@ -182,6 +210,8 @@ class EnergyCorrectionModel(BaseModel): class MomentumModel(BaseModel): + model_config = ConfigDict(extra="forbid") + axes: Sequence[str] bins: Sequence[int] ranges: Sequence[Sequence[int]] @@ -192,6 +222,8 @@ class MomentumModel(BaseModel): sigma_radius: int class MomentumCalibrationModel(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None kx_scale: float ky_scale: float @@ -205,6 +237,8 @@ class MomentumCalibrationModel(BaseModel): calibration: Optional[MomentumCalibrationModel] = None class MomentumCorrectionModel(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None feature_points: Sequence[Sequence[float]] rotation_symmetry: int @@ -215,6 +249,8 @@ class MomentumCorrectionModel(BaseModel): class DelayModel(BaseModel): + model_config = ConfigDict(extra="forbid") + adc_range: Sequence[int] flip_time_axis: bool # Group keys in the datafile @@ -224,6 +260,8 @@ class DelayModel(BaseModel): t0_key: Optional[str] = None class DelayCalibration(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None adc_range: Sequence[int] delay_range: Sequence[float] @@ -234,6 +272,8 @@ class DelayCalibration(BaseModel): calibration: Optional[DelayCalibration] = None class DelayOffsets(BaseModel): + model_config = ConfigDict(extra="forbid") + creation_date: Optional[float] = None constant: Optional[float] = None flip_delay_axis: Optional[bool] = False @@ -251,6 +291,8 @@ class OffsetColumn(BaseModel): class MetadataModel(BaseModel): + model_config = ConfigDict(extra="forbid") + archiver_url: Optional[HttpUrl] = None token: Optional[SecretStr] = None epics_pvs: Optional[Sequence[str]] = None @@ -262,6 +304,8 @@ class MetadataModel(BaseModel): class NexusModel(BaseModel): + model_config = ConfigDict(extra="forbid") + reader: str # prob good to have validation here # Currently only NXmpes definition is supported definition: Literal["NXmpes"] @@ -269,6 +313,8 @@ class NexusModel(BaseModel): class ConfigModel(BaseModel): + model_config = ConfigDict(extra="forbid") + core: CoreModel dataframe: DataframeModel energy: EnergyModel @@ -279,5 +325,3 @@ class ConfigModel(BaseModel): metadata: Optional[MetadataModel] = None nexus: Optional[NexusModel] = None static: Optional[StaticModel] = None - - model_config = ConfigDict(extra="forbid") diff --git a/sed/config/default.yaml b/sed/config/default.yaml index 3d591926..28b9be3b 100644 --- a/sed/config/default.yaml +++ b/sed/config/default.yaml @@ -88,8 +88,6 @@ delay: # value ranges of the analog-to-digital converter axes used for encoding the delay stage position # (in unbinned coordinates) adc_range: [1900, 25600] - # pump probe time overlap in ps - time0: 0 # if to flip the time axis flip_time_axis: False From e3577bb8784b3637ba6f936819ddf086dbcdb644 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 23:10:46 +0200 Subject: [PATCH 212/300] fix flash loader --- sed/config/flash_example_config.yaml | 2 +- sed/loader/flash/dataframe.py | 2 +- sed/loader/flash/loader.py | 6 +-- sed/loader/flash/utils.py | 2 +- sed/loader/utils.py | 8 +-- tests/data/loader/flash/config.yaml | 51 +++----------------- tests/loader/flash/test_dataframe_creator.py | 2 +- tests/loader/flash/test_flash_loader.py | 2 +- 8 files changed, 18 insertions(+), 57 deletions(-) diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 185fac78..0bc95dd2 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -45,7 +45,7 @@ dataframe: tof_binning: 8 # Binning parameter for time-of-flight data # Columns used for jitter correction - jitter_columns: [dldPosX, dldPosY, dldTimeSteps] + jitter_cols: [dldPosX, dldPosY, dldTimeSteps] # Column settings columns: diff --git a/sed/loader/flash/dataframe.py b/sed/loader/flash/dataframe.py index 887cb9dd..6501c82a 100644 --- a/sed/loader/flash/dataframe.py +++ b/sed/loader/flash/dataframe.py @@ -248,7 +248,7 @@ def df_train(self) -> pd.DataFrame: aux_alias = self._config.get("aux_alias", "dldAux") if channel == aux_alias: try: - sub_channels = self._config["channels"][aux_alias]["subChannels"] + sub_channels = self._config["channels"][aux_alias]["sub_channels"] except KeyError: raise KeyError( f"Provide 'subChannels' for auxiliary channel '{aux_alias}'.", diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index 9b3524bc..f451783f 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -110,7 +110,7 @@ def _initialize_dirs(self) -> None: ) from exc beamtime_dir = Path( - self._config["dataframe"]["beamtime_dir"][self._config["core"]["beamline"]], + self._config["static"]["beamtime_dir"][self._config["core"]["beamline"]], ) beamtime_dir = beamtime_dir.joinpath(f"{year}/data/{beamtime_id}/") @@ -175,7 +175,7 @@ def get_files_from_run_id( # type: ignore[override] FileNotFoundError: If no files are found for the given run in the directory. """ # Define the stream name prefixes based on the data acquisition identifier - stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] + stream_name_prefixes = self._config["static"]["stream_name_prefixes"] if folders is None: folders = self._config["core"]["base_folder"] @@ -183,7 +183,7 @@ def get_files_from_run_id( # type: ignore[override] if isinstance(folders, str): folders = [folders] - daq = self._config["dataframe"].get("daq") + daq = self._config["dataframe"]["daq"] # Generate the file patterns to search for in the directory file_pattern = f"{stream_name_prefixes[daq]}_run{run_id}_*." + extension diff --git a/sed/loader/flash/utils.py b/sed/loader/flash/utils.py index 6eb2ac30..85bca9a4 100644 --- a/sed/loader/flash/utils.py +++ b/sed/loader/flash/utils.py @@ -78,7 +78,7 @@ def get_channels( if format_ == FORMATS[2] and aux_alias in available_channels: if extend_aux: channels.extend( - channel_dict[aux_alias]["subChannels"].keys(), + channel_dict[aux_alias]["sub_channels"].keys(), ) else: channels.extend([aux_alias]) diff --git a/sed/loader/utils.py b/sed/loader/utils.py index 6bcce9f8..4f18cf0f 100644 --- a/sed/loader/utils.py +++ b/sed/loader/utils.py @@ -160,9 +160,9 @@ def split_dld_time_from_sector_id( Args: df (pd.DataFrame | dask.dataframe.DataFrame): Dataframe to use. tof_column (str, optional): Name of the column containing the - time-of-flight steps. Defaults to config["dataframe"]["tof_column"]. + time-of-flight steps. Defaults to config["dataframe"]["columns"]["tof"]. sector_id_column (str, optional): Name of the column containing the - sectorID. Defaults to config["dataframe"]["sector_id_column"]. + sectorID. Defaults to config["dataframe"]["columns"]["sector_id"]. sector_id_reserved_bits (int, optional): Number of bits reserved for the config (dict, optional): Dataframe configuration dictionary. Defaults to None. @@ -172,11 +172,11 @@ def split_dld_time_from_sector_id( if tof_column is None: if config is None: raise ValueError("Either tof_column or config must be given.") - tof_column = config["tof_column"] + tof_column = config["columns"]["tof"] if sector_id_column is None: if config is None: raise ValueError("Either sector_id_column or config must be given.") - sector_id_column = config["sector_id_column"] + sector_id_column = config["columns"]["sector_id"] if sector_id_reserved_bits is None: if config is None: raise ValueError("Either sector_id_reserved_bits or config must be given.") diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index d4209636..f7296ea5 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -29,34 +29,10 @@ dataframe: # if true, removes the 3 bits reserved for dldSectorID from the dldTimeSteps column split_sector_id_from_dld_time: True sector_id_reserved_bits: 3 - # dataframe column containing x coordinates - x_column: dldPosX - # dataframe column containing corrected x coordinates - corrected_x_column: "X" - # dataframe column containing kx coordinates - kx_column: "kx" - # dataframe column containing y coordinates - - y_column: dldPosY - # dataframe column containing corrected y coordinates - corrected_y_column: "Y" - # dataframe column containing kx coordinates - ky_column: "ky" - # dataframe column containing time-of-flight data - - tof_column: dldTimeSteps - # dataframe column containing time-of-flight data in ns - tof_ns_column: dldTime - # dataframe column containing corrected time-of-flight data - corrected_tof_column: "tm" - # the time stamp column - time_stamp_alias: timeStamp # time length of a base time-of-flight bin in seconds tof_binwidth: 2.0576131995767355E-11 # binning parameter for time-of-flight data. 2**tof_binning bins per base bin tof_binning: 3 # power of 2, 4 means 8 bins per step - # dataframe column containing sector ID. obtained from dldTimeSteps column - sector_id_column: dldSectorID sector_delays: [0., 0., 0., 0., 0., 0., 0., 0.] @@ -125,32 +101,27 @@ dataframe: # The auxiliary channel has a special structure where the group further contains # a multidimensional structure so further aliases are defined below dldAux: - format: per_train + format: per_pulse index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 - subChannels: + sub_channels: sampleBias: slice: 0 - dtype: float64 + dtype: float32 tofVoltage: slice: 1 dtype: float64 extractorVoltage: slice: 2 - dtype: float64 extractorCurrent: slice: 3 - dtype: float64 cryoTemperature: slice: 4 - dtype: float64 sampleTemperature: slice: 5 - dtype: float64 dldTimeBinSize: slice: 15 - dtype: float64 timeStamp: format: per_train @@ -173,8 +144,10 @@ dataframe: dataset_key: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/value" slice: 0 + +# (Not to be changed by user) +static: # The prefixes of the stream names for different DAQ systems for parsing filenames - # (Not to be changed by user) stream_name_prefixes: pbd: "GMD_DATA_gmd_data" pbd2: "FL2PhotDiag_pbd2_gmd_data" @@ -188,15 +161,3 @@ dataframe: # (Not to be changed by user) beamtime_dir: pg2: "/asap3/flash/gpfs/pg2/" - -# metadata collection from scicat -# metadata: -# scicat_url: -# scicat_username: -# scicat_password: - -# The nexus collection routine shall be finalized soon for both instruments -# nexus: -# reader: "flash" -# definition: "NXmpes" -# input_files: ["NXmpes_config_HEXTOF_light.json"] diff --git a/tests/loader/flash/test_dataframe_creator.py b/tests/loader/flash/test_dataframe_creator.py index 64e7712c..fe1c8f79 100644 --- a/tests/loader/flash/test_dataframe_creator.py +++ b/tests/loader/flash/test_dataframe_creator.py @@ -234,7 +234,7 @@ def test_create_dataframe_per_train(config_dataframe: dict, h5_paths: list[Path] # The subchannels are stored in the second dimension # Only index amount of values are stored in the first dimension, the rest are NaNs # hence the slicing - subchannels = config_dataframe["channels"]["dldAux"]["subChannels"] + subchannels = config_dataframe["channels"]["dldAux"]["sub_channels"] for subchannel, values in subchannels.items(): assert np.all(df.df_train[subchannel].dropna().values == data[: key.size, values["slice"]]) diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index a34a9977..3d9a5170 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -33,7 +33,7 @@ def test_initialize_dirs( config_["core"]["year"] = "2000" # Find base path of beamline from config. Here, we use pg2 - base_path = config_["dataframe"]["beamtime_dir"]["pg2"] + base_path = config_["static"]["beamtime_dir"]["pg2"] expected_path = ( Path(base_path) / config_["core"]["year"] / "data" / config_["core"]["beamtime_id"] ) From 8054bfa276bc1c7328fdc5eb0cef199f31032cd0 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 23:16:58 +0200 Subject: [PATCH 213/300] fix calibrator tests again --- tests/calibrator/test_delay.py | 2 +- tests/calibrator/test_energy.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index 5ec9fc2c..b5bbe49c 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -131,7 +131,7 @@ def test_delay_parameters_from_delay_range_mm() -> None: delay_stage_vals = np.linspace(0, 99, 100) cfg = { "core": {"loader": "flash"}, - "dataframe": {"delay_column": "delay"}, + "dataframe": {"columns": {"delay": "delay"}}, "delay": { "offsets": { "constant": 1, diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 9ec24229..21bac331 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -294,8 +294,10 @@ def test_append_tof_ns_axis() -> None: """ cfg = { "dataframe": { - "tof_column": "t", - "tof_ns_column": "t_ns", + "columns": { + "tof": "t", + "tof_ns": "t_ns", + }, "tof_binning": 2, "tof_binwidth": 1e-9, }, From 35dcd115dbec2b681fcede93428d1610e0a46d9c Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 12 Oct 2024 23:53:07 +0200 Subject: [PATCH 214/300] fix processor tests --- sed/config/config_model.py | 23 ++++++++++++++++++++--- sed/config/mpes_example_config.yaml | 3 +-- sed/core/processor.py | 4 ++-- tests/test_processor.py | 7 ++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index d2cbcd96..4d9a006a 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -9,7 +9,6 @@ from pydantic import DirectoryPath from pydantic import field_validator from pydantic import FilePath -from pydantic import HttpUrl from pydantic import NewPath from pydantic import SecretStr @@ -106,6 +105,8 @@ class DataframeModel(BaseModel): jitter_cols: Sequence[str] jitter_amps: Union[float, Sequence[float]] timed_dataframe_unit_time: float + first_event_time_stamp_key: Optional[str] = None + ms_markers_key: Optional[str] = None # flash specific settings forward_fill_iterations: Optional[int] = None ubid_offset: Optional[int] = None @@ -172,6 +173,8 @@ class EnergyCalibrationModel(BaseModel): d: Optional[float] = None t0: Optional[float] = None E0: Optional[float] = None + coeffs: Optional[Sequence[float]] = None + offset: Optional[float] = None energy_scale: str calibration: Optional[EnergyCalibrationModel] = None @@ -244,9 +247,23 @@ class MomentumCorrectionModel(BaseModel): rotation_symmetry: int include_center: bool use_center: bool + ascale: Optional[Sequence[float]] = None + center_point: Optional[Sequence[float]] = None + outer_points: Optional[Sequence[Sequence[float]]] = None correction: Optional[MomentumCorrectionModel] = None + class MomentumTransformationModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + creation_date: Optional[float] = None + scale: Optional[float] = None + angle: Optional[float] = None + xtrans: Optional[float] = None + ytrans: Optional[float] = None + + transformations: Optional[MomentumTransformationModel] = None + class DelayModel(BaseModel): model_config = ConfigDict(extra="forbid") @@ -293,7 +310,7 @@ class OffsetColumn(BaseModel): class MetadataModel(BaseModel): model_config = ConfigDict(extra="forbid") - archiver_url: Optional[HttpUrl] = None + archiver_url: Optional[str] = None token: Optional[SecretStr] = None epics_pvs: Optional[Sequence[str]] = None fa_in_channel: Optional[str] = None @@ -309,7 +326,7 @@ class NexusModel(BaseModel): reader: str # prob good to have validation here # Currently only NXmpes definition is supported definition: Literal["NXmpes"] - input_files: Sequence[FilePath] + input_files: Sequence[str] class ConfigModel(BaseModel): diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 3d1d3e39..1d3753d9 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -15,8 +15,6 @@ core: gid: 1001 dataframe: - # dataframe column name for the time stamp column - time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occurring at every millisecond (used to calculate timestamps) ms_markers_key: "msMarkers" # hdf5 attribute containing the timestamp of the first event in a file @@ -38,6 +36,7 @@ dataframe: ky: ky # dataframe column containing ky coordinates energy: energy # dataframe column containing energy data delay: delay # dataframe column containing delay data + timestamp: timeStamps # dataframe column containing timestamp data # time length of a base time-of-flight bin in ns tof_binwidth: 4.125e-12 # Binning factor of the tof_column-data compared to tof_binwidth diff --git a/sed/core/processor.py b/sed/core/processor.py index e4d5f550..f1294ce9 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1192,8 +1192,8 @@ def load_bias_series( ): raise ValueError( "If binned_data is provided as an xarray, it needs to contain dimensions " - f"'{self._config['dataframe']['tof_column']}' and " - f"'{self._config['dataframe']['bias_column']}'!.", + f"'{self._config['dataframe']['columns']['tof']}' and " + f"'{self._config['dataframe']['columns']['bias']}'!.", ) tof = binned_data.coords[self._config["dataframe"]["columns"]["tof"]].values biases = binned_data.coords[self._config["dataframe"]["columns"]["bias"]].values diff --git a/tests/test_processor.py b/tests/test_processor.py index 7070dead..9471f721 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -660,8 +660,9 @@ def test_align_dld_sectors() -> None: user_config={}, system_config={}, ) - config["core"]["paths"]["processed"] = ( - config["core"]["paths"]["processed"] + "_align_dld_sectors" + config["core"]["paths"]["processed"] = Path( + config["core"]["paths"]["processed"], + "_align_dld_sectors", ) processor = SedProcessor( folder=df_folder + "../flash/", @@ -998,7 +999,7 @@ def test_compute_with_normalization() -> None: def test_get_normalization_histogram() -> None: """Test the generation function for the normalization histogram""" - config = {"core": {"loader": "mpes"}, "dataframe": {"time_stamp_alias": "timeStamps"}} + config = {"core": {"loader": "mpes"}, "dataframe": {"columns": {"timestamp": "timeStamps"}}} processor = SedProcessor( folder=df_folder, config=config, From 738cd85f7380d9184e698c6c146b53f18fd765cb Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 13 Oct 2024 00:02:46 +0200 Subject: [PATCH 215/300] fix sxp loader --- sed/config/config_model.py | 1 + sed/config/sxp_example_config.yaml | 28 +++++++++++++++++---------- sed/loader/sxp/loader.py | 6 +++--- tests/data/loader/sxp/config.yaml | 30 +++++++++++++++++------------ tests/loader/sxp/test_sxp_loader.py | 4 ++-- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 4d9a006a..2b716ee0 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -82,6 +82,7 @@ class ChannelModel(BaseModel): index_key: Optional[str] = None slice: Optional[int] = None dtype: Optional[str] = None + max_hits: Optional[int] = None class subChannel(BaseModel): model_config = ConfigDict(extra="forbid") diff --git a/sed/config/sxp_example_config.yaml b/sed/config/sxp_example_config.yaml index c0757fa5..1a7baa90 100644 --- a/sed/config/sxp_example_config.yaml +++ b/sed/config/sxp_example_config.yaml @@ -28,20 +28,27 @@ dataframe: daq: DA03 forward_fill_iterations: 2 num_trains: 10 - x_column: dldPosX - corrected_x_column: "X" - kx_column: "kx" - y_column: dldPosY - corrected_y_column: "Y" - ky_column: "ky" - tof_column: dldTimeSteps - tof_ns_column: dldTime - corrected_tof_column: "tm" - bias_column: "sampleBias" tof_binwidth: 6.875E-12 # in seconds tof_binning: 1 jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] + # Column settings + columns: + x: dldPosX + corrected_x: X + kx: kx + y: dldPosY + corrected_y: Y + ky: ky + tof: dldTimeSteps + tof_ns: dldTime + corrected_tof: tm + timestamp: timeStamp + auxiliary: dldAux + sector_id: dldSectorID + delay: delayStage + corrected_delay: pumpProbeTime + units: dldPosX: 'step' dldPosY: 'step' @@ -96,6 +103,7 @@ dataframe: dataset_key: "/CONTROL/SCS_ILH_LAS/MDL/OPTICALDELAY_PP800/actualPosition/value" index_key: "/INDEX/trainId" +static: stream_name_prefixes: DA03: "RAW-R" stream_name_postfixes: diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index a77e8ed6..c9d73a4c 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -116,7 +116,7 @@ def _initialize_dirs(self): ) from exc beamtime_dir = Path( - self._config["dataframe"]["beamtime_dir"][self._config["core"]["beamline"]], + self._config["static"]["beamtime_dir"][self._config["core"]["beamline"]], ) beamtime_dir = beamtime_dir.joinpath(f"{year}/{beamtime_id}/") @@ -158,8 +158,8 @@ def get_files_from_run_id( FileNotFoundError: If no files are found for the given run in the directory. """ # Define the stream name prefixes based on the data acquisition identifier - stream_name_prefixes = self._config["dataframe"]["stream_name_prefixes"] - stream_name_postfixes = self._config["dataframe"].get("stream_name_postfixes", {}) + stream_name_prefixes = self._config["static"]["stream_name_prefixes"] + stream_name_postfixes = self._config["static"].get("stream_name_postfixes", {}) if isinstance(run_id, (int, np.integer)): run_id = str(run_id).zfill(4) diff --git a/tests/data/loader/sxp/config.yaml b/tests/data/loader/sxp/config.yaml index 095178ff..c5680847 100644 --- a/tests/data/loader/sxp/config.yaml +++ b/tests/data/loader/sxp/config.yaml @@ -4,28 +4,33 @@ core: paths: raw: "tests/data/loader/sxp/" processed: "tests/data/loader/sxp/parquet" - -binning: num_cores: 10 dataframe: ubid_offset: 0 daq: DA03 forward_fill_iterations: 2 - x_column: dldPosX - corrected_x_column: "X" - kx_column: "kx" - y_column: dldPosY - corrected_y_column: "Y" - ky_column: "ky" - tof_column: dldTimeSteps - tof_ns_column: dldTime - corrected_tof_column: "tm" - bias_column: "sampleBias" tof_binwidth: 2.0576131995767355E-11 # in seconds tof_binning: 3 jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] + # Column settings + columns: + x: dldPosX + corrected_x: X + kx: kx + y: dldPosY + corrected_y: Y + ky: ky + tof: dldTimeSteps + tof_ns: dldTime + corrected_tof: tm + timestamp: timeStamp + auxiliary: dldAux + sector_id: dldSectorID + delay: delayStage + corrected_delay: pumpProbeTime + units: dldPosX: 'step' dldPosY: 'step' @@ -78,6 +83,7 @@ dataframe: dataset_key: "/CONTROL/SCS_ILH_LAS/MDL/OPTICALDELAY_PP800/actualPosition/value" index_key: "/INDEX/trainId" +static: stream_name_prefixes: DA03: "RAW-R" stream_name_postfixes: diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 09588152..85a31eac 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -87,7 +87,7 @@ def test_initialize_dirs(config_file: dict, fs) -> None: config["core"]["year"] = "2000" # Find base path of beamline from config. - base_path = config["dataframe"]["beamtime_dir"]["sxp"] + base_path = config["static"]["beamtime_dir"]["sxp"] expected_path = Path(base_path) / config["core"]["year"] / config["core"]["beamtime_id"] # Create expected paths expected_raw_path = expected_path / "raw" @@ -150,7 +150,7 @@ def test_data_keys_not_in_h5(config_file: dict, key_type: str): sl = SXPLoader(config=config) with pytest.raises(ValueError) as e: - sl.create_dataframe_per_file(config["core"]["paths"]["raw"] + H5_PATH) + sl.create_dataframe_per_file(Path(config["core"]["paths"]["raw"], H5_PATH)) assert str(e.value.args[0]) == f"The {key_type} for channel dldPosX does not exist." From bc6f45799eabed540f6e4f93a7122a5475f2638a Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 13 Oct 2024 00:28:21 +0200 Subject: [PATCH 216/300] update notebooks --- sed/config/config_model.py | 4 +++- sed/config/sxp_example_config.yaml | 4 ++-- ...ion_pipeline_for_example_time-resolved_ARPES_data.ipynb | 5 +---- tutorial/3_metadata_collection_and_export_to_NeXus.ipynb | 7 ++----- tutorial/4_hextof_workflow.ipynb | 4 +++- tutorial/5_sxp_workflow.ipynb | 6 +++--- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 2b716ee0..1ef3c4e4 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -33,7 +33,7 @@ class CoreModel(BaseModel): paths: Optional[Paths] = None num_cores: Optional[int] = None year: Optional[int] = None - beamtime_id: Optional[int] = None + beamtime_id: Optional[Union[int, str]] = None instrument: Optional[str] = None beamline: Optional[str] = None # TODO: move copy tool to a separate model @@ -83,6 +83,7 @@ class ChannelModel(BaseModel): slice: Optional[int] = None dtype: Optional[str] = None max_hits: Optional[int] = None + scale: Optional[float] = None class subChannel(BaseModel): model_config = ConfigDict(extra="forbid") @@ -115,6 +116,7 @@ class DataframeModel(BaseModel): sector_id_reserved_bits: Optional[int] = None sector_delays: Optional[Sequence[float]] = None daq: Optional[str] = None + num_trains: Optional[int] = None # write validator for model so that x_column gets converted to columns: x diff --git a/sed/config/sxp_example_config.yaml b/sed/config/sxp_example_config.yaml index 1a7baa90..b9e4d298 100644 --- a/sed/config/sxp_example_config.yaml +++ b/sed/config/sxp_example_config.yaml @@ -7,9 +7,9 @@ core: beamline: sxp instrument: sxp paths: - data_raw_dir: "/path/to/data" + raw: "/path/to/data" # change this to a local directory where you want to store the parquet files - data_parquet_dir: "/path/to/parquet" + processed: "/path/to/parquet" binning: # Histogram computation mode to use. diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 82798d55..28865853 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -59,7 +59,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", verbose=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={}, verbose=True)" ] }, { @@ -649,9 +649,6 @@ } ], "metadata": { - "interpreter": { - "hash": "728003ee06929e5fa5ff815d1b96bf487266025e4b7440930c6bf4536d02d243" - }, "kernelspec": { "display_name": "python3", "language": "python", diff --git a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb index 98c1258c..7ac5d39f 100644 --- a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb +++ b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb @@ -143,7 +143,7 @@ "outputs": [], "source": [ "# create sed processor using the config file, and collect the meta data from the files:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", metadata=metadata, collect_metadata=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={}, metadata=metadata, collect_metadata=True)" ] }, { @@ -290,9 +290,6 @@ } ], "metadata": { - "interpreter": { - "hash": "728003ee06929e5fa5ff815d1b96bf487266025e4b7440930c6bf4536d02d243" - }, "kernelspec": { "display_name": "python3", "language": "python", @@ -308,7 +305,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.9.19" } }, "nbformat": 4, diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index c9b36c04..e6484602 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -31,6 +31,8 @@ }, "outputs": [], "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", "from typing import List\n", "from pathlib import Path\n", "import os\n", @@ -964,7 +966,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.9", + "display_name": "python3", "language": "python", "name": "python3" }, diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index bb7c6a7b..3241545f 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -53,8 +53,8 @@ "config = {\n", " \"core\": {\n", " \"paths\": {\n", - " \"data_raw_dir\": \"/gpfs/exfel/exp/SXP/202302/p004316/raw/\",\n", - " \"data_parquet_dir\": os.path.expanduser(\"~\") + \"/sxp_parquet/\",\n", + " \"raw\": \"/gpfs/exfel/exp/SXP/202302/p004316/raw/\",\n", + " \"processed\": os.path.expanduser(\"~\") + \"/sxp_parquet/\",\n", " }\n", " }\n", "}\n", @@ -394,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.9.19" } }, "nbformat": 4, From bc40fea71674381735f0e265bccedd1ba89f259b Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 13 Oct 2024 01:03:52 +0200 Subject: [PATCH 217/300] fix remaining tests --- sed/config/config_model.py | 2 +- sed/loader/mpes/loader.py | 2 +- tests/data/loader/generic/config.yaml | 3 +- tests/data/loader/mpes/config.yaml | 44 +++++++++++---------------- tests/loader/test_loaders.py | 23 +++++++++----- tests/test_diagnostics.py | 9 ++++-- 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 1ef3c4e4..61aed957 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -22,7 +22,7 @@ class Paths(BaseModel): model_config = ConfigDict(extra="forbid") raw: DirectoryPath - processed: Union[DirectoryPath, NewPath] + processed: Optional[Union[DirectoryPath, NewPath]] = None class CoreModel(BaseModel): diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index eaf296f0..4f5ed94b 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -749,7 +749,7 @@ def get_files_from_run_id( ) if folders is None: - folders = self._config["core"]["paths"]["data_raw_dir"] + folders = str(self._config["core"]["paths"]["raw"]) if isinstance(folders, str): folders = [folders] diff --git a/tests/data/loader/generic/config.yaml b/tests/data/loader/generic/config.yaml index e901b4d3..e1c6163f 100644 --- a/tests/data/loader/generic/config.yaml +++ b/tests/data/loader/generic/config.yaml @@ -1 +1,2 @@ -test: +core: + loader: generic diff --git a/tests/data/loader/mpes/config.yaml b/tests/data/loader/mpes/config.yaml index 1a411be2..62e46ebc 100644 --- a/tests/data/loader/mpes/config.yaml +++ b/tests/data/loader/mpes/config.yaml @@ -1,10 +1,8 @@ core: paths: - data_raw_dir: "tests/data/loader/mpes/" + raw: "tests/data/loader/mpes/" dataframe: - # dataframe column name for the time stamp column - time_stamp_alias: "timeStamps" # hdf5 group name containing eventIDs occuring at every millisecond (used to calculate timestamps) ms_markers_key: "msMarkers" # hdf5 attribute containing the timestamp of the first event in a file @@ -13,30 +11,6 @@ dataframe: timed_dataframe_unit_time: 0.001 # list of columns to apply jitter to jitter_cols: ["X", "Y", "t", "ADC"] - # dataframe column containing x coordinates - x_column: "X" - # dataframe column containing y coordinates - y_column: "Y" - # dataframe column containing time-of-flight data - tof_column: "t" - # dataframe column containing analog-to-digital data - adc_column: "ADC" - # dataframe column containing bias voltage data - bias_column: "sampleBias" - # dataframe column containing corrected x coordinates - corrected_x_column: "Xm" - # dataframe column containing corrected y coordinates - corrected_y_column: "Ym" - # dataframe column containing corrected time-of-flight data - corrected_tof_column: "tm" - # dataframe column containing kx coordinates - kx_column: "kx" - # dataframe column containing ky coordinates - ky_column: "ky" - # dataframe column containing energy data - energy_column: "energy" - # dataframe column containing delay data - delay_column: "delay" # time length of a base time-of-flight bin in ns tof_binwidth: 4.125e-12 # Binning factor of the tof_column-data compared to tof_binwidth (2^(tof_binning-1)) @@ -44,6 +18,22 @@ dataframe: # binning factor used for the adc coordinate (2^(adc_binning-1)) adc_binning: 3 # Default units for dataframe entries + + columns: + x: X # dataframe column containing x coordinates + y: Y # dataframe column containing y coordinates + tof: t # dataframe column containing time-of-flight data + adc: ADC # dataframe column containing analog-to-digital data + bias: sampleBias # dataframe column containing bias voltage data + corrected_x: Xm # dataframe column containing corrected x coordinates + corrected_y: Ym # dataframe column containing corrected y coordinates + corrected_tof: tm # dataframe column containing corrected time-of-flight data + kx: kx # dataframe column containing kx coordinates + ky: ky # dataframe column containing ky coordinates + energy: energy # dataframe column containing energy data + delay: delay # dataframe column containing delay data + timestamp: timeStamps # dataframe column containing timestamp data + units: X: 'step' Y: 'step' diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 734e7b44..f3e0bf84 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -70,6 +70,9 @@ def get_all_loaders() -> list[ParameterSet]: loader_name, "config.yaml", ), + folder_config={}, + user_config={}, + system_config={}, ), ) for loader_name in get_names_of_all_loaders() @@ -95,8 +98,9 @@ def test_has_correct_read_dataframe_func(loader: BaseLoader, read_type: str) -> # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["processed"] = ( - config["core"]["paths"]["processed"] + f"_{read_type}" + config["core"]["paths"]["processed"] = Path( + config["core"]["paths"]["processed"], + f"_{read_type}", ) loader = get_loader(loader_name=loader.__name__, config=config) @@ -183,8 +187,9 @@ def test_timed_dataframe(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["processed"] = ( - config["core"]["paths"]["processed"] + "_timed_dataframe" + config["core"]["paths"]["processed"] = Path( + config["core"]["paths"]["processed"], + "_timed_dataframe", ) loader = get_loader(loader_name=loader.__name__, config=config) @@ -226,7 +231,10 @@ def test_get_count_rate(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["processed"] = config["core"]["paths"]["processed"] + "_count_rate" + config["core"]["paths"]["processed"] = Path( + config["core"]["paths"]["processed"], + "_count_rate", + ) loader = get_loader(loader_name=loader.__name__, config=config) if loader.__name__ != "BaseLoader": @@ -273,8 +281,9 @@ def test_get_elapsed_time(loader: BaseLoader) -> None: # Fix for race condition during parallel testing if loader.__name__ in {"flash", "sxp"}: config = deepcopy(loader._config) # pylint: disable=protected-access - config["core"]["paths"]["processed"] = ( - config["core"]["paths"]["processed"] + "_elapsed_time" + config["core"]["paths"]["processed"] = Path( + config["core"]["paths"]["processed"], + "_elapsed_time", ) loader = get_loader(loader_name=loader.__name__, config=config) diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 2614e69d..cb5bcce6 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -18,7 +18,12 @@ df_folder = package_dir + "/../tests/data/loader/mpes/" folder = package_dir + "/../tests/data/calibrator/" files = glob.glob(df_folder + "*.h5") -config = parse_config(package_dir + "/../tests/data/config/config.yaml") +config = parse_config( + package_dir + "/../tests/data/loader/mpes/config.yaml", + folder_config={}, + user_config={}, + system_config={}, +) loader = get_loader("mpes", config=config) @@ -39,7 +44,7 @@ def test_plot_histogram(ncols: int, backend: str) -> None: bins = config["histogram"]["bins"] for loc, axis in enumerate(axes): if axis.startswith("@"): - axes[loc] = config["dataframe"].get(axis.strip("@")) + axes[loc] = config["dataframe"]["columns"].get(axis.strip("@")) values = {axis: dataframe[axis].compute() for axis in axes} grid_histogram(values, ncols, axes, bins, ranges, backend) From b6db85f6eefd68fdbe0b61884171b4fd283fe695 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 13 Oct 2024 21:11:19 +0200 Subject: [PATCH 218/300] add config model for copy tool --- .cspell/custom-dictionary.txt | 1 + sed/config/config_model.py | 39 +++++++++++++++++++++++------ sed/config/mpes_example_config.yaml | 16 ++++++------ sed/core/processor.py | 15 ++++------- sed/loader/mpes/loader.py | 1 - tests/test_processor.py | 18 ++----------- 6 files changed, 47 insertions(+), 43 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index f4cc3e5c..5fe97cab 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -126,6 +126,7 @@ ftype fwhm genindex getgid +getgrgid getmtime gpfs griddata diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 61aed957..b471e732 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -1,4 +1,5 @@ """Pydantic model to validate the config for SED package.""" +import grp from collections.abc import Sequence from typing import Literal from typing import Optional @@ -18,29 +19,45 @@ ## https://github.com/astral-sh/ruff/issues/5434 -class Paths(BaseModel): +class PathsModel(BaseModel): model_config = ConfigDict(extra="forbid") raw: DirectoryPath processed: Optional[Union[DirectoryPath, NewPath]] = None +class CopyToolModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + source: DirectoryPath + dest: DirectoryPath + safety_margin: Optional[float] = None + gid: Optional[int] = None + scheduler: Optional[str] = None + + @field_validator("gid") + @classmethod + def validate_gid(cls, v: int) -> int: + """Checks if the gid is valid on the system""" + try: + grp.getgrgid(v) + except KeyError: + raise ValueError(f"Invalid value {v} for gid. Group not found.") + return v + + class CoreModel(BaseModel): model_config = ConfigDict(extra="forbid") loader: str verbose: Optional[bool] = None - paths: Optional[Paths] = None + paths: Optional[PathsModel] = None num_cores: Optional[int] = None year: Optional[int] = None beamtime_id: Optional[Union[int, str]] = None instrument: Optional[str] = None beamline: Optional[str] = None - # TODO: move copy tool to a separate model - use_copy_tool: Optional[bool] = None - copy_tool_source: Optional[DirectoryPath] = None - copy_tool_dest: Optional[DirectoryPath] = None - copy_tool_kwds: Optional[dict] = None + copy_tool: Optional[CopyToolModel] = None @field_validator("loader") @classmethod @@ -51,6 +68,14 @@ def validate_loader(cls, v: str) -> str: raise ValueError(f"Invalid loader {v}. Available loaders are: {names}") return v + @field_validator("num_cores") + @classmethod + def validate_num_cores(cls, v: int) -> int: + """Checks if the num_cores field is a positive integer""" + if v < 1: + raise ValueError(f"Invalid value {v} for num_cores. Needs to be > 0.") + return v + class ColumnsModel(BaseModel): model_config = ConfigDict(extra="forbid") diff --git a/sed/config/mpes_example_config.yaml b/sed/config/mpes_example_config.yaml index 1d3753d9..c45ca865 100644 --- a/sed/config/mpes_example_config.yaml +++ b/sed/config/mpes_example_config.yaml @@ -4,15 +4,13 @@ core: # Number of parallel threads to use for parallelized jobs (e.g. binning, data conversion, copy, ...) num_cores: 20 # Option to use the copy tool to mirror data to a local storage location before processing. - use_copy_tool: False - # path to the root of the source data directory - copy_tool_source: null # "/path/to/data/" - # path to the root or the local data storage - copy_tool_dest: null # "/path/to/localDataStore/" - # optional keywords for the copy tool: - copy_tool_kwds: - # group id to set for copied files and folders - gid: 1001 + # copy_tool: + # # path to the root of the source data directory + # source: "/path/to/data/" + # # path to the root or the local data storage + # dest: "/path/to/localDataStore/" + # # group id to set for copied files and folders + # gid: 1000 dataframe: # hdf5 group name containing eventIDs occurring at every millisecond (used to calculate timestamps) diff --git a/sed/core/processor.py b/sed/core/processor.py index f1294ce9..03b7308b 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -161,22 +161,17 @@ def __init__( verbose=self._verbose, ) - self.use_copy_tool = self._config.get("core", {}).get( - "use_copy_tool", - False, - ) + self.use_copy_tool = "copy_tool" in self._config["core"] if self.use_copy_tool: try: self.ct = CopyTool( - source=self._config["core"]["copy_tool_source"], - dest=self._config["core"]["copy_tool_dest"], num_cores=self._config["core"]["num_cores"], - **self._config["core"].get("copy_tool_kwds", {}), + **self._config["core"]["copy_tool"], ) logger.debug( - f"Initialized copy tool: Copy file from " - f"'{self._config['core']['copy_tool_source']}' " - f"to '{self._config['core']['copy_tool_dest']}'.", + f"Initialized copy tool: Copy files from " + f"'{self._config['core']['copy_tool']['source']}' " + f"to '{self._config['core']['copy_tool']['dest']}'.", ) except KeyError: self.use_copy_tool = False diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 4f5ed94b..16ad573d 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -72,7 +72,6 @@ def hdf5_to_dataframe( electron_channels = [] column_names = [] - print("Print values: ", channels) for name, channel in channels.items(): if channel["format"] == "per_electron": if channel["dataset_key"] in test_proc: diff --git a/tests/test_processor.py b/tests/test_processor.py index 9471f721..a98fa882 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -219,7 +219,7 @@ def test_attributes_setters() -> None: def test_copy_tool() -> None: """Test the copy tool functionality in the processor""" - config = {"core": {"loader": "mpes", "use_copy_tool": True}} + config: dict[str, dict[str, Any]] = {"core": {"loader": "mpes"}} processor = SedProcessor( config=config, folder_config={}, @@ -231,10 +231,7 @@ def test_copy_tool() -> None: config = { "core": { "loader": "mpes", - "use_copy_tool": True, - "copy_tool_source": source_folder, - "copy_tool_dest": dest_folder, - "copy_tool_kwds": {"gid": os.getgid()}, + "copy_tool": {"source": source_folder, "dest": dest_folder, "gid": os.getgid()}, }, } processor = SedProcessor( @@ -248,17 +245,6 @@ def test_copy_tool() -> None: processor.load(files=files) assert processor.files[0].find(dest_folder) > -1 - # test illegal keywords: - config["core"]["copy_tool_kwds"] = {"gid": os.getgid(), "illegal_keyword": True} - with pytest.raises(TypeError): - processor = SedProcessor( - config=config, - folder_config={}, - user_config={}, - system_config={}, - verbose=True, - ) - feature4 = np.array([[203.2, 341.96], [299.16, 345.32], [304.38, 149.88], [199.52, 152.48]]) feature5 = np.array( From bdc5bac5d778cff3c79aa300ded69c94a4bed55d Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 13 Oct 2024 23:42:53 +0200 Subject: [PATCH 219/300] Add further type refinements to config model --- sed/calibrator/delay.py | 14 +++---- sed/calibrator/energy.py | 22 ++++------- sed/calibrator/momentum.py | 26 +++++-------- sed/config/config_model.py | 75 +++++++++++++++++++------------------- sed/core/processor.py | 64 ++++++++++++++++++++------------ sed/loader/mpes/loader.py | 4 +- 6 files changed, 102 insertions(+), 103 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 13c4436f..8035d456 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -146,7 +146,7 @@ def append_delay_axis( or datafile is not None ): calibration = {} - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now() if adc_range is not None: calibration["adc_range"] = adc_range if delay_range is not None: @@ -158,9 +158,7 @@ def append_delay_axis( else: # report usage of loaded parameters if "creation_date" in calibration and not suppress_output: - datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = calibration["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using delay calibration parameters generated on {datestring}") if adc_column is None: @@ -212,7 +210,7 @@ def append_delay_axis( ) if not suppress_output: logger.info(f"Converted delay_range (ps) = {calibration['delay_range']}") - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now() if "delay_range" in calibration.keys(): df[delay_column] = calibration["delay_range"][0] + ( @@ -285,7 +283,7 @@ def add_offsets( # pylint:disable=duplicate-code # use passed parameters, overwrite config offsets = {} - offsets["creation_date"] = datetime.now().timestamp() + offsets["creation_date"] = datetime.now() # column-based offsets if columns is not None: offsets["columns"] = {} @@ -331,9 +329,7 @@ def add_offsets( offsets["flip_delay_axis"] = flip_delay_axis elif "creation_date" in offsets and not suppress_output: - datestring = datetime.fromtimestamp(offsets["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = offsets["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using delay offset parameters generated on {datestring}") if len(offsets) > 0: diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 72cfa801..b004d25e 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -612,7 +612,7 @@ def calibrate( else: raise NotImplementedError() - self.calibration["creation_date"] = datetime.now().timestamp() + self.calibration["creation_date"] = datetime.now() return self.calibration def view( @@ -843,12 +843,10 @@ def append_energy_axis( if len(kwds) > 0: for key, value in kwds.items(): calibration[key] = value - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now() elif "creation_date" in calibration and not suppress_output: - datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = calibration["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using energy calibration parameters generated on {datestring}") # try to determine calibration type if not provided @@ -1202,7 +1200,7 @@ def common_apply_func(apply: bool): # noqa: ARG001 self.correction["amplitude"] = correction["amplitude"] self.correction["center"] = correction["center"] self.correction["correction_type"] = correction["correction_type"] - self.correction["creation_date"] = datetime.now().timestamp() + self.correction["creation_date"] = datetime.now() amplitude_slider.close() x_center_slider.close() y_center_slider.close() @@ -1440,12 +1438,10 @@ def apply_energy_correction( for key, value in kwds.items(): correction[key] = value - correction["creation_date"] = datetime.now().timestamp() + correction["creation_date"] = datetime.now() elif "creation_date" in correction and not suppress_output: - datestring = datetime.fromtimestamp(correction["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = correction["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using energy correction parameters generated on {datestring}") missing_keys = {"correction_type", "center", "amplitude"} - set(correction.keys()) @@ -1592,7 +1588,7 @@ def add_offsets( # pylint:disable=duplicate-code # use passed parameters, overwrite config offsets = {} - offsets["creation_date"] = datetime.now().timestamp() + offsets["creation_date"] = datetime.now() # column-based offsets if columns is not None: offsets["columns"] = {} @@ -1637,9 +1633,7 @@ def add_offsets( raise TypeError(f"Invalid type for constant: {type(constant)}") elif "creation_date" in offsets and not suppress_output: - datestring = datetime.fromtimestamp(offsets["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = offsets["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using energy offset parameters generated on {datestring}") if len(offsets) > 0: diff --git a/sed/calibrator/momentum.py b/sed/calibrator/momentum.py index f1852d8a..e2a40e38 100644 --- a/sed/calibrator/momentum.py +++ b/sed/calibrator/momentum.py @@ -678,7 +678,7 @@ def spline_warp_estimate( if self.pouter_ord is None: if self.pouter is not None: self.pouter_ord = po.pointset_order(self.pouter) - self.correction["creation_date"] = datetime.now().timestamp() + self.correction["creation_date"] = datetime.now() else: try: features = np.asarray( @@ -693,11 +693,7 @@ def spline_warp_estimate( ascale = np.asarray(ascale) if "creation_date" in self.correction: - datestring = datetime.fromtimestamp( - self.correction["creation_date"], - ).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = self.correction["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info( "No landmarks defined, using momentum correction parameters " f"generated on {datestring}", @@ -715,7 +711,7 @@ def spline_warp_estimate( self.add_features(features=features, rotsym=rotsym) else: - self.correction["creation_date"] = datetime.now().timestamp() + self.correction["creation_date"] = datetime.now() if ascale is not None: if isinstance(ascale, (int, float, np.floating, np.integer)): @@ -1135,9 +1131,7 @@ def pose_adjustment( ) elif "creation_date" in transformations: - datestring = datetime.fromtimestamp(transformations["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = transformations["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using transformation parameters generated on {datestring}") def update(scale: float, xtrans: float, ytrans: float, angle: float): @@ -1257,7 +1251,7 @@ def apply_func(apply: bool): # noqa: ARG001 fig.canvas.draw_idle() if transformations != self.transformations: - transformations["creation_date"] = datetime.now().timestamp() + transformations["creation_date"] = datetime.now() self.transformations = transformations if self._verbose: @@ -1714,7 +1708,7 @@ def calibrate( # Assemble into return dictionary self.calibration = {} - self.calibration["creation_date"] = datetime.now().timestamp() + self.calibration["creation_date"] = datetime.now() self.calibration["kx_axis"] = k_row self.calibration["ky_axis"] = k_col self.calibration["grid"] = (k_rowgrid, k_colgrid) @@ -1825,7 +1819,7 @@ def gather_correction_metadata(self) -> dict: pass if len(self.adjust_params) > 0: metadata["registration"] = self.adjust_params - metadata["registration"]["creation_date"] = datetime.now().timestamp() + metadata["registration"]["creation_date"] = datetime.now() metadata["registration"]["applied"] = True metadata["registration"]["depends_on"] = ( "/entry/process/registration/transformations/rot_z" @@ -1952,15 +1946,13 @@ def append_k_axis( ]: if key in kwds: calibration[key] = kwds.pop(key) - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now() if len(kwds) > 0: raise TypeError(f"append_k_axis() got unexpected keyword arguments {kwds.keys()}.") if "creation_date" in calibration and not suppress_output: - datestring = datetime.fromtimestamp(calibration["creation_date"]).strftime( - "%m/%d/%Y, %H:%M:%S", - ) + datestring = calibration["creation_date"].strftime("%m/%d/%Y, %H:%M:%S") logger.info(f"Using momentum calibration parameters generated on {datestring}") try: diff --git a/sed/config/config_model.py b/sed/config/config_model.py index b471e732..5fde6513 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -1,6 +1,7 @@ """Pydantic model to validate the config for SED package.""" import grp from collections.abc import Sequence +from datetime import datetime from typing import Literal from typing import Optional from typing import Union @@ -132,6 +133,7 @@ class DataframeModel(BaseModel): jitter_cols: Sequence[str] jitter_amps: Union[float, Sequence[float]] timed_dataframe_unit_time: float + # mpes specific settings first_event_time_stamp_key: Optional[str] = None ms_markers_key: Optional[str] = None # flash specific settings @@ -149,11 +151,11 @@ class DataframeModel(BaseModel): class BinningModel(BaseModel): model_config = ConfigDict(extra="forbid") - hist_mode: str - mode: str + hist_mode: Literal["numpy", "numba"] + mode: Literal["fast", "lean", "legacy"] pbar: bool threads_per_worker: int - threadpool_API: str + threadpool_API: Literal["blas", "openmp"] class HistogramModel(BaseModel): @@ -185,36 +187,34 @@ class EnergyModel(BaseModel): normalize_order: int fastdtw_radius: int peak_window: int - calibration_method: str - energy_scale: str + calibration_method: Literal["lmfit", "lstsq", "lsq"] + energy_scale: Literal["binding", "kinetic"] tof_fermi: int - tof_width: Sequence[int] - x_width: Sequence[int] - y_width: Sequence[int] + tof_width: tuple[int, int] + x_width: tuple[int, int] + y_width: tuple[int, int] color_clip: int bias_key: Optional[str] = None class EnergyCalibrationModel(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None + creation_date: Optional[datetime] = None d: Optional[float] = None t0: Optional[float] = None E0: Optional[float] = None coeffs: Optional[Sequence[float]] = None offset: Optional[float] = None - energy_scale: str + energy_scale: Literal["binding", "kinetic"] calibration: Optional[EnergyCalibrationModel] = None class EnergyOffsets(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None + creation_date: Optional[datetime] = None constant: Optional[float] = None - ## This seems rather complicated way to define offsets, - # inconsistent in how args vs config are for add_offsets class OffsetColumn(BaseModel): weight: float preserve_mean: bool @@ -227,10 +227,10 @@ class OffsetColumn(BaseModel): class EnergyCorrectionModel(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None - correction_type: str + creation_date: Optional[datetime] = None + correction_type: Literal["Gaussian", "Lorentzian", "spherical", "Lorentzian_asymmetric"] amplitude: float - center: Sequence[float] + center: tuple[float, float] gamma: Optional[float] = None sigma: Optional[float] = None diameter: Optional[float] = None @@ -245,9 +245,9 @@ class MomentumModel(BaseModel): axes: Sequence[str] bins: Sequence[int] - ranges: Sequence[Sequence[int]] - detector_ranges: Sequence[Sequence[int]] - center_pixel: Sequence[int] + ranges: Sequence[tuple[int, int]] + detector_ranges: Sequence[tuple[int, int]] + center_pixel: tuple[int, int] sigma: int fwhm: int sigma_radius: int @@ -255,7 +255,7 @@ class MomentumModel(BaseModel): class MomentumCalibrationModel(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None + creation_date: Optional[datetime] = None kx_scale: float ky_scale: float x_center: float @@ -270,21 +270,21 @@ class MomentumCalibrationModel(BaseModel): class MomentumCorrectionModel(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None - feature_points: Sequence[Sequence[float]] + creation_date: Optional[datetime] = None + feature_points: Sequence[tuple[float, float]] rotation_symmetry: int include_center: bool use_center: bool ascale: Optional[Sequence[float]] = None - center_point: Optional[Sequence[float]] = None - outer_points: Optional[Sequence[Sequence[float]]] = None + center_point: Optional[tuple[float, float]] = None + outer_points: Optional[Sequence[tuple[float, float]]] = None correction: Optional[MomentumCorrectionModel] = None class MomentumTransformationModel(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None + creation_date: Optional[datetime] = None scale: Optional[float] = None angle: Optional[float] = None xtrans: Optional[float] = None @@ -296,7 +296,7 @@ class MomentumTransformationModel(BaseModel): class DelayModel(BaseModel): model_config = ConfigDict(extra="forbid") - adc_range: Sequence[int] + adc_range: tuple[int, int] flip_time_axis: bool # Group keys in the datafile p1_key: Optional[str] = None @@ -307,24 +307,22 @@ class DelayModel(BaseModel): class DelayCalibration(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None - adc_range: Sequence[int] - delay_range: Sequence[float] - time0: float - delay_range_mm: Sequence[float] - datafile: FilePath # .h5 extension in filepath + creation_date: Optional[datetime] = None + adc_range: Optional[tuple[int, int]] = None + delay_range: Optional[tuple[float, float]] = None + time0: Optional[float] = None + delay_range_mm: Optional[tuple[float, float]] = None + datafile: Optional[FilePath] # .h5 extension in filepath calibration: Optional[DelayCalibration] = None class DelayOffsets(BaseModel): model_config = ConfigDict(extra="forbid") - creation_date: Optional[float] = None + creation_date: Optional[datetime] = None constant: Optional[float] = None flip_delay_axis: Optional[bool] = False - ## This seems rather complicated way to define offsets, - # inconsistent in how args vs config are for add_offsets class OffsetColumn(BaseModel): weight: float preserve_mean: bool @@ -344,14 +342,15 @@ class MetadataModel(BaseModel): fa_in_channel: Optional[str] = None fa_hor_channel: Optional[str] = None ca_in_channel: Optional[str] = None - aperture_config: Optional[dict] = None - lens_mode_config: Optional[dict] = None + aperture_config: Optional[dict[datetime, dict]] = None + lens_mode_config: Optional[dict[str, dict]] = None class NexusModel(BaseModel): model_config = ConfigDict(extra="forbid") - reader: str # prob good to have validation here + # Currently only mpes reader is supported + reader: Literal["mpes"] # Currently only NXmpes definition is supported definition: Literal["NXmpes"] input_files: Sequence[str] diff --git a/sed/core/processor.py b/sed/core/processor.py index 03b7308b..d5f4c418 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -5,6 +5,7 @@ import pathlib from collections.abc import Sequence +from copy import deepcopy from datetime import datetime from typing import Any from typing import cast @@ -698,11 +699,13 @@ def save_splinewarp( correction[key] = [] for point in value: correction[key].append([float(i) for i in point]) + elif key == "creation_date": + correction[key] = value.isoformat() else: correction[key] = float(value) if "creation_date" not in correction: - correction["creation_date"] = datetime.now().timestamp() + correction["creation_date"] = datetime.now().isoformat() config = { "momentum": { @@ -785,10 +788,13 @@ def save_transformations( raise ValueError("No momentum transformation parameters to save!") transformations = {} for key, value in self.mc.transformations.items(): - transformations[key] = float(value) + if key == "creation_date": + transformations[key] = value.isoformat() + else: + transformations[key] = float(value) if "creation_date" not in transformations: - transformations["creation_date"] = datetime.now().timestamp() + transformations["creation_date"] = datetime.now().isoformat() config = { "momentum": { @@ -933,11 +939,13 @@ def save_momentum_calibration( for key, value in self.mc.calibration.items(): if key in ["kx_axis", "ky_axis", "grid", "extent"]: continue - - calibration[key] = float(value) + elif key == "creation_date": + calibration[key] = value.isoformat() + else: + calibration[key] = float(value) if "creation_date" not in calibration: - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now().isoformat() config = {"momentum": {"calibration": calibration}} save_config(config, filename, overwrite) @@ -1069,16 +1077,18 @@ def save_energy_correction( if len(self.ec.correction) == 0: raise ValueError("No energy correction parameters to save!") correction = {} - for key, val in self.ec.correction.items(): + for key, value in self.ec.correction.items(): if key == "correction_type": - correction[key] = val + correction[key] = value elif key == "center": - correction[key] = [float(i) for i in val] + correction[key] = [float(i) for i in value] + elif key == "creation_date": + correction[key] = value.isoformat() else: - correction[key] = float(val) + correction[key] = float(value) if "creation_date" not in correction: - correction["creation_date"] = datetime.now().timestamp() + correction["creation_date"] = datetime.now().isoformat() config = {"energy": {"correction": correction}} save_config(config, filename, overwrite) @@ -1430,11 +1440,13 @@ def save_energy_calibration( calibration[key] = value elif key == "coeffs": calibration[key] = [float(i) for i in value] + elif key == "creation_date": + calibration[key] = value.isoformat() else: calibration[key] = float(value) if "creation_date" not in calibration: - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now().isoformat() config = {"energy": {"calibration": calibration}} save_config(config, filename, overwrite) @@ -1595,10 +1607,14 @@ def save_energy_offset( if len(self.ec.offsets) == 0: raise ValueError("No energy offset parameters to save!") - if "creation_date" not in self.ec.offsets.keys(): - self.ec.offsets["creation_date"] = datetime.now().timestamp() + offsets = deepcopy(self.ec.offsets) + + if "creation_date" not in offsets.keys(): + offsets["creation_date"] = datetime.now() - config = {"energy": {"offsets": self.ec.offsets}} + offsets["creation_date"] = offsets["creation_date"].isoformat() + + config = {"energy": {"offsets": offsets}} save_config(config, filename, overwrite) logger.info(f'Saved energy offset parameters to "{filename}".') @@ -1791,11 +1807,13 @@ def save_delay_calibration( calibration[key] = value elif key in ["adc_range", "delay_range", "delay_range_mm"]: calibration[key] = [float(i) for i in value] + elif key == "creation_date": + calibration[key] = value.isoformat() else: calibration[key] = float(value) if "creation_date" not in calibration: - calibration["creation_date"] = datetime.now().timestamp() + calibration["creation_date"] = datetime.now().isoformat() config = { "delay": { @@ -1898,14 +1916,14 @@ def save_delay_offsets( if len(self.dc.offsets) == 0: raise ValueError("No delay offset parameters to save!") - if "creation_date" not in self.ec.offsets.keys(): - self.ec.offsets["creation_date"] = datetime.now().timestamp() + offsets = deepcopy(self.dc.offsets) - config = { - "delay": { - "offsets": self.dc.offsets, - }, - } + if "creation_date" not in offsets.keys(): + offsets["creation_date"] = datetime.now() + + offsets["creation_date"] = offsets["creation_date"].isoformat() + + config = {"delay": {"offsets": offsets}} save_config(config, filename, overwrite) logger.info(f'Saved delay offset parameters to "{filename}".') diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 16ad573d..14a41ee2 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -853,7 +853,7 @@ def gather_metadata( # Get metadata from Epics archive if not present already epics_channels = self._config["metadata"]["epics_pvs"] - start = datetime.datetime.utcfromtimestamp(ts_from).isoformat() + start = datetime.datetime.utcfromtimestamp(ts_from) channels_missing = set(epics_channels) - set( metadata["file"].keys(), @@ -891,7 +891,7 @@ def gather_metadata( # Determine the correct aperture_config stamps = sorted( - list(self._config["metadata"]["aperture_config"]) + [start], + list(self._config["metadata"]["aperture_config"].keys()) + [start], ) current_index = stamps.index(start) timestamp = stamps[current_index - 1] # pick last configuration before file date From 022dc69406043047c6251453c88dfb64ca67a21d Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 14 Oct 2024 22:10:14 +0200 Subject: [PATCH 220/300] add tests for config model --- tests/test_config.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 0ab54cbb..5ebf259b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,6 +9,7 @@ from pathlib import Path import pytest +from pydantic import ValidationError from sed.core.config import complete_dictionary from sed.core.config import load_config @@ -16,6 +17,10 @@ from sed.core.config import save_config package_dir = os.path.dirname(find_spec("sed").origin) + +test_config_dir = Path(package_dir).joinpath("../tests/data/loader/") +config_paths = test_config_dir.glob("*/*.yaml") + default_config_keys = [ "binning", "histogram", @@ -140,3 +145,28 @@ def test_save_dict() -> None: save_config(config_dict, filename, overwrite=True) config = load_config(filename) assert "test_entry" not in config.keys() + + +@pytest.mark.parametrize("config_path", config_paths) +def test_config_model_valid(config_path) -> None: + """Test the config model for a valid config.""" + config = parse_config(config_path, verify_config=True) + assert config is not None + + +def test_invalid_config_extra_field(): + """Test that an invalid config with an extra field fails validation.""" + default_config = parse_config(verify_config=False) + invalid_config = default_config.copy() + invalid_config["extra_field"] = "extra_value" + with pytest.raises(ValidationError): + parse_config(invalid_config, verify_config=True) + + +def test_invalid_config_missing_field(): + """Test that an invalid config with a missing required field fails validation.""" + default_config = parse_config(verify_config=False) + invalid_config = default_config.copy() + del invalid_config["core"]["loader"] + with pytest.raises(ValidationError): + parse_config(default_config=invalid_config, verify_config=True) From 52a11dcec657db9bbdb773ea970a030340ccec0d Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 14 Oct 2024 23:51:33 +0200 Subject: [PATCH 221/300] fix remaining tutorials --- tutorial/6_binning_with_time-stamped_data.ipynb | 4 ++-- tutorial/7_correcting_orthorhombic_symmetry.ipynb | 7 ++----- tutorial/8_jittering_tutorial.ipynb | 7 ++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index 93080372..7978568f 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True, verbose=True)" + "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)" ] }, { @@ -162,7 +162,7 @@ "sp.load_bias_series(data_files=files, normalize=True, biases=voltages, ranges=[(64000, 76000)])\n", "rg = (65500, 66000)\n", "sp.find_bias_peaks(ranges=rg, ref_id=5, infer_others=True, apply=True)\n", - "sp.calibrate_energy_axis(ref_energy=-0.5, ref_id=4, energy_scale=\"kinetic\", method=\"lmfit\")" + "sp.calibrate_energy_axis(ref_energy=-0.5, energy_scale=\"kinetic\", method=\"lmfit\")" ] }, { diff --git a/tutorial/7_correcting_orthorhombic_symmetry.ipynb b/tutorial/7_correcting_orthorhombic_symmetry.ipynb index a385b3d1..faba30b6 100644 --- a/tutorial/7_correcting_orthorhombic_symmetry.ipynb +++ b/tutorial/7_correcting_orthorhombic_symmetry.ipynb @@ -60,7 +60,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", time_stamps=True, verbose=True)\n", + "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)\n", "sp.add_jitter()" ] }, @@ -216,11 +216,8 @@ } ], "metadata": { - "interpreter": { - "hash": "728003ee06929e5fa5ff815d1b96bf487266025e4b7440930c6bf4536d02d243" - }, "kernelspec": { - "display_name": "python3", + "display_name": ".pyenv", "language": "python", "name": "python3" }, diff --git a/tutorial/8_jittering_tutorial.ipynb b/tutorial/8_jittering_tutorial.ipynb index ef11af7a..d98a19b2 100644 --- a/tutorial/8_jittering_tutorial.ipynb +++ b/tutorial/8_jittering_tutorial.ipynb @@ -58,7 +58,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\")" + "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={})" ] }, { @@ -358,11 +358,8 @@ } ], "metadata": { - "interpreter": { - "hash": "728003ee06929e5fa5ff815d1b96bf487266025e4b7440930c6bf4536d02d243" - }, "kernelspec": { - "display_name": "python3", + "display_name": ".pyenv", "language": "python", "name": "python3" }, From 9edbea2aa686d6e846118b57e5c82ef4230aaebc Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 10:08:15 +0200 Subject: [PATCH 222/300] fix config model tests --- tests/test_config.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 5ebf259b..df93520f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -150,23 +150,28 @@ def test_save_dict() -> None: @pytest.mark.parametrize("config_path", config_paths) def test_config_model_valid(config_path) -> None: """Test the config model for a valid config.""" - config = parse_config(config_path, verify_config=True) + config = parse_config(config_path, user_config={}, system_config={}, verify_config=True) assert config is not None def test_invalid_config_extra_field(): """Test that an invalid config with an extra field fails validation.""" - default_config = parse_config(verify_config=False) + default_config = parse_config(user_config={}, system_config={}, verify_config=True) invalid_config = default_config.copy() invalid_config["extra_field"] = "extra_value" with pytest.raises(ValidationError): - parse_config(invalid_config, verify_config=True) + parse_config(invalid_config, user_config={}, system_config={}, verify_config=True) def test_invalid_config_missing_field(): """Test that an invalid config with a missing required field fails validation.""" - default_config = parse_config(verify_config=False) + default_config = parse_config(user_config={}, system_config={}, verify_config=True) invalid_config = default_config.copy() del invalid_config["core"]["loader"] with pytest.raises(ValidationError): - parse_config(default_config=invalid_config, verify_config=True) + parse_config( + user_config={}, + system_config={}, + default_config=invalid_config, + verify_config=True, + ) From ad8705d360004d07af3af8c07cb8c767226b8b41 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 12:26:15 +0200 Subject: [PATCH 223/300] add review suggestions --- sed/config/config_model.py | 16 +++++----------- sed/config/flash_example_config.yaml | 2 +- sed/core/config.py | 2 +- sed/loader/mpes/loader.py | 2 +- tests/data/loader/flash/config.yaml | 2 +- tests/test_processor.py | 3 +++ 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 5fde6513..43e69e3d 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -11,7 +11,9 @@ from pydantic import DirectoryPath from pydantic import field_validator from pydantic import FilePath +from pydantic import HttpUrl from pydantic import NewPath +from pydantic import PositiveInt from pydantic import SecretStr from sed.loader.loader_interface import get_names_of_all_loaders @@ -53,7 +55,7 @@ class CoreModel(BaseModel): loader: str verbose: Optional[bool] = None paths: Optional[PathsModel] = None - num_cores: Optional[int] = None + num_cores: Optional[PositiveInt] = None year: Optional[int] = None beamtime_id: Optional[Union[int, str]] = None instrument: Optional[str] = None @@ -69,14 +71,6 @@ def validate_loader(cls, v: str) -> str: raise ValueError(f"Invalid loader {v}. Available loaders are: {names}") return v - @field_validator("num_cores") - @classmethod - def validate_num_cores(cls, v: int) -> int: - """Checks if the num_cores field is a positive integer""" - if v < 1: - raise ValueError(f"Invalid value {v} for num_cores. Needs to be > 0.") - return v - class ColumnsModel(BaseModel): model_config = ConfigDict(extra="forbid") @@ -336,7 +330,7 @@ class OffsetColumn(BaseModel): class MetadataModel(BaseModel): model_config = ConfigDict(extra="forbid") - archiver_url: Optional[str] = None + archiver_url: Optional[HttpUrl] = None token: Optional[SecretStr] = None epics_pvs: Optional[Sequence[str]] = None fa_in_channel: Optional[str] = None @@ -353,7 +347,7 @@ class NexusModel(BaseModel): reader: Literal["mpes"] # Currently only NXmpes definition is supported definition: Literal["NXmpes"] - input_files: Sequence[str] + input_files: Sequence[FilePath] class ConfigModel(BaseModel): diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 0bc95dd2..bdd6a827 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -140,7 +140,7 @@ dataframe: # The auxiliary channel has a special structure where the group further contains # a multidimensional structure so further aliases are defined below dldAux: - format: per_pulse + format: per_train index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 diff --git a/sed/core/config.py b/sed/core/config.py index ff11bbd7..723ef598 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -148,7 +148,7 @@ def parse_config( return config_dict # Run the config through the ConfigModel to ensure it is valid config_model = ConfigModel(**config_dict) - return config_model.model_dump(exclude_unset=True, exclude_none=True, exclude_defaults=True) + return config_model.model_dump(exclude_unset=True, exclude_none=True) def load_config(config_path: str) -> dict: diff --git a/sed/loader/mpes/loader.py b/sed/loader/mpes/loader.py index 14a41ee2..a7cb82d0 100644 --- a/sed/loader/mpes/loader.py +++ b/sed/loader/mpes/loader.py @@ -861,7 +861,7 @@ def gather_metadata( for channel in channels_missing: try: _, vals = get_archiver_data( - archiver_url=self._config["metadata"].get("archiver_url"), + archiver_url=str(self._config["metadata"].get("archiver_url")), archiver_channel=channel, ts_from=ts_from, ts_to=ts_to, diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index f7296ea5..db3aabbb 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -101,7 +101,7 @@ dataframe: # The auxiliary channel has a special structure where the group further contains # a multidimensional structure so further aliases are defined below dldAux: - format: per_pulse + format: per_train index_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/index" dataset_key: "/uncategorised/FLASH.EXP/HEXTOF.DAQ/DLD1/value" slice: 4 diff --git a/tests/test_processor.py b/tests/test_processor.py index a98fa882..6cc1d06c 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -829,6 +829,7 @@ def test_add_time_stamped_data() -> None: system_config={}, time_stamps=True, verbose=True, + verify_config=False, ) df_ts = processor.dataframe.timeStamps.compute().values data = np.linspace(0, 1, 20) @@ -1055,12 +1056,14 @@ def test_save(caplog) -> None: folder_config={}, user_config=package_dir + "/../sed/config/mpes_example_config.yaml", system_config={}, + verify_config=False, ) config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"][ "MCPfront" ] = 21.0 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["Z1"] = 2450 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["F"] = 69.23 + config["nexus"]["input_files"] = [df_folder + "../../../../sed/config/NXmpes_config.json"] processor = SedProcessor( folder=df_folder, config=config, From 7aa7231a58aed5fb7774708d1cbbddc04749fef6 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 15:49:58 +0200 Subject: [PATCH 224/300] fix reporting of energy/delay offsets --- sed/calibrator/delay.py | 15 ++++++--------- sed/calibrator/energy.py | 19 ++++++------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 8035d456..9e0be940 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -357,19 +357,16 @@ def add_offsets( ) log_str += f"\n Flip delay axis: {flip_delay_axis}" elif k == "columns": - for k2, v2 in offsets["columns"].items(): - columns.append(k2) - try: - weight = v2["weight"] - except KeyError: - weight = 1 + for column_name, column_dict in offsets["columns"].items(): + columns.append(column_name) + weight = column_dict.get("weight", 1) weights.append(weight) - pm = v2.get("preserve_mean", False) + pm = column_dict.get("preserve_mean", False) preserve_mean.append(pm) - red = v2.get("reduction", None) + red = column_dict.get("reduction", None) reductions.append(red) log_str += ( - f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"\n Column[{column_name}]: Weight={weight}, Preserve Mean: {pm}, " f"Reductions: {red}." ) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index b004d25e..831baa94 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -1652,12 +1652,9 @@ def add_offsets( constant = v * scale_sign log_str += f"\n Constant: {constant}" elif k == "columns": - for k2, v2 in offsets["columns"].items(): - columns.append(k2) - try: - weight = v2["weight"] - except KeyError: - weight = 1 + for column_name, column_dict in offsets["columns"].items(): + columns.append(column_name) + weight = column_dict.get("weight", 1) if not isinstance(weight, (int, float, np.integer, np.floating)): raise TypeError( f"Invalid type for weight of column {k}: {type(weight)}", @@ -1665,18 +1662,14 @@ def add_offsets( # flip sign if binding energy scale weight = weight * scale_sign weights.append(weight) - pm = v2.get("preserve_mean", False) - if str(pm).lower() in ["false", "0", "no"]: - pm = False - elif str(pm).lower() in ["true", "1", "yes"]: - pm = True + pm = column_dict.get("preserve_mean", False) preserve_mean.append(pm) - red = v2.get("reduction", None) + red = column_dict.get("reduction", None) if str(red).lower() in ["none", "null"]: red = None reductions.append(red) log_str += ( - f"\n Column[{k}]: Weight={weight}, Preserve Mean: {pm}, " + f"\n Column[{column_name}]: Weight={weight}, Preserve Mean: {pm}, " f"Reductions: {red}." ) From ec20e03661a07e172f76cc7fadece13c18c3409d Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 16:30:59 +0200 Subject: [PATCH 225/300] fix handling of nexus input files and tests --- sed/core/processor.py | 2 +- tests/test_processor.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/sed/core/processor.py b/sed/core/processor.py index d5f4c418..0ea90517 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -2528,7 +2528,7 @@ def save( ) input_files = kwds.pop( "input_files", - self._config["nexus"]["input_files"], + [str(path) for path in self._config["nexus"]["input_files"]], ) except KeyError as exc: raise ValueError( diff --git a/tests/test_processor.py b/tests/test_processor.py index 6cc1d06c..709fc048 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -1063,7 +1063,7 @@ def test_save(caplog) -> None: ] = 21.0 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["Z1"] = 2450 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["F"] = 69.23 - config["nexus"]["input_files"] = [df_folder + "../../../../sed/config/NXmpes_config.json"] + config["nexus"]["input_files"] = [package_dir + "/../sed/config/NXmpes_config.json"] processor = SedProcessor( folder=df_folder, config=config, @@ -1095,7 +1095,6 @@ def test_save(caplog) -> None: # and error if any validation problems occur. processor.save( "output.nxs", - input_files=df_folder + "../../../../sed/config/NXmpes_config.json", fail=True, ) assert os.path.isfile("output.nxs") @@ -1104,7 +1103,6 @@ def test_save(caplog) -> None: with pytest.raises(ValidationFailed): processor.save( "result.nxs", - input_files=df_folder + "../../../../sed/config/NXmpes_config.json", fail=True, ) # Check that the issues are raised as warnings per default: @@ -1113,7 +1111,7 @@ def test_save(caplog) -> None: yaml.dump({"Instrument": {"undocumented_field": "undocumented entry"}}, f) with open("temp_config.json", "w") as f: with open( - df_folder + "../../../../sed/config/NXmpes_config.json", + package_dir + "/../sed/config/NXmpes_config.json", encoding="utf-8", ) as stream: config_dict = json.load(stream) From 4e535cf0fec203fda6a6b792e6f7d3bbfe1ad68b Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 23:09:00 +0200 Subject: [PATCH 226/300] fix error reporting --- sed/calibrator/delay.py | 4 ++++ sed/calibrator/energy.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sed/calibrator/delay.py b/sed/calibrator/delay.py index 9e0be940..14758b41 100644 --- a/sed/calibrator/delay.py +++ b/sed/calibrator/delay.py @@ -360,6 +360,10 @@ def add_offsets( for column_name, column_dict in offsets["columns"].items(): columns.append(column_name) weight = column_dict.get("weight", 1) + if not isinstance(weight, (int, float, np.integer, np.floating)): + raise TypeError( + f"Invalid type for weight of column {column_name}: {type(weight)}", + ) weights.append(weight) pm = column_dict.get("preserve_mean", False) preserve_mean.append(pm) diff --git a/sed/calibrator/energy.py b/sed/calibrator/energy.py index 831baa94..65493f66 100644 --- a/sed/calibrator/energy.py +++ b/sed/calibrator/energy.py @@ -1657,7 +1657,7 @@ def add_offsets( weight = column_dict.get("weight", 1) if not isinstance(weight, (int, float, np.integer, np.floating)): raise TypeError( - f"Invalid type for weight of column {k}: {type(weight)}", + f"Invalid type for weight of column {column_name}: {type(weight)}", ) # flip sign if binding energy scale weight = weight * scale_sign From 7a7441c41f101dc9be30ce6702344f5d8e51c5ef Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 17 Oct 2024 14:48:16 +0200 Subject: [PATCH 227/300] fix sxp notebook --- tutorial/5_sxp_workflow.ipynb | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index 3241545f..7e3d2dbc 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -297,7 +297,6 @@ "outputs": [], "source": [ "sp.calibrate_energy_axis(\n", - " ref_id=5,\n", " ref_energy=-33,\n", " method=\"lmfit\",\n", " energy_scale='kinetic',\n", @@ -316,15 +315,6 @@ "sp.save_energy_calibration()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sp.append_energy_axis()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -341,7 +331,7 @@ "sp.load(runs=np.arange(58, 62))\n", "sp.add_jitter()\n", "sp.filter_column(\"pulseId\", max_value=756)\n", - "sp.append_energy_axis()" + "sp.append_energy_axis(bias_voltage=957)" ] }, { From fb04ce6ddf8a828d4f01cdfa5c2950ab56621d92 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 21 Oct 2024 22:48:06 +0200 Subject: [PATCH 228/300] changes from review --- sed/config/config_model.py | 22 ++++----- sed/config/flash_example_config.yaml | 6 +-- sed/core/config.py | 2 +- sed/core/processor.py | 8 ++-- sed/loader/sxp/loader.py | 4 +- tests/data/loader/flash/config.yaml | 2 - tests/test_config.py | 68 ++++++++++++++++++++++++++-- 7 files changed, 83 insertions(+), 29 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 43e69e3d..5a840f3b 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -137,9 +137,8 @@ class DataframeModel(BaseModel): sector_id_reserved_bits: Optional[int] = None sector_delays: Optional[Sequence[float]] = None daq: Optional[str] = None - num_trains: Optional[int] = None - - # write validator for model so that x_column gets converted to columns: x + # SXP specific settings + num_trains: Optional[PositiveInt] = None class BinningModel(BaseModel): @@ -148,14 +147,14 @@ class BinningModel(BaseModel): hist_mode: Literal["numpy", "numba"] mode: Literal["fast", "lean", "legacy"] pbar: bool - threads_per_worker: int + threads_per_worker: PositiveInt threadpool_API: Literal["blas", "openmp"] class HistogramModel(BaseModel): model_config = ConfigDict(extra="forbid") - bins: Sequence[int] + bins: Sequence[PositiveInt] axes: Sequence[str] ranges: Sequence[tuple[float, float]] @@ -174,8 +173,8 @@ class StaticModel(BaseModel): class EnergyModel(BaseModel): model_config = ConfigDict(extra="forbid") - bins: int - ranges: Sequence[int] + bins: PositiveInt + ranges: tuple[int, int] normalize: bool normalize_span: int normalize_order: int @@ -238,7 +237,7 @@ class MomentumModel(BaseModel): model_config = ConfigDict(extra="forbid") axes: Sequence[str] - bins: Sequence[int] + bins: Sequence[PositiveInt] ranges: Sequence[tuple[int, int]] detector_ranges: Sequence[tuple[int, int]] center_pixel: tuple[int, int] @@ -266,7 +265,7 @@ class MomentumCorrectionModel(BaseModel): creation_date: Optional[datetime] = None feature_points: Sequence[tuple[float, float]] - rotation_symmetry: int + rotation_symmetry: PositiveInt include_center: bool use_center: bool ascale: Optional[Sequence[float]] = None @@ -275,7 +274,7 @@ class MomentumCorrectionModel(BaseModel): correction: Optional[MomentumCorrectionModel] = None - class MomentumTransformationModel(BaseModel): + class MomentumTransformationsModel(BaseModel): model_config = ConfigDict(extra="forbid") creation_date: Optional[datetime] = None @@ -284,7 +283,7 @@ class MomentumTransformationModel(BaseModel): xtrans: Optional[float] = None ytrans: Optional[float] = None - transformations: Optional[MomentumTransformationModel] = None + transformations: Optional[MomentumTransformationsModel] = None class DelayModel(BaseModel): @@ -295,7 +294,6 @@ class DelayModel(BaseModel): # Group keys in the datafile p1_key: Optional[str] = None p2_key: Optional[str] = None - p3_key: Optional[str] = None t0_key: Optional[str] = None class DelayCalibration(BaseModel): diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index bdd6a827..0f12402b 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -1,8 +1,8 @@ # This file contains the default configuration for the flash loader. core: # defines the loader - loader: 'flash' - # # Since this will run on maxwell most probably, we have a lot of cores at our disposal + loader: flash + # Since this will run on maxwell most probably, we have a lot of cores at our disposal num_cores: 100 # the beamline where experiment took place beamline: pg2 @@ -202,7 +202,7 @@ dataframe: # The nexus collection routine shall be finalized soon for both instruments # nexus: -# reader: "flash" +# reader: "mpes" # definition: "NXmpes" # input_files: ["NXmpes_config_HEXTOF_light.json"] diff --git a/sed/core/config.py b/sed/core/config.py index 723ef598..ca29285c 100644 --- a/sed/core/config.py +++ b/sed/core/config.py @@ -63,7 +63,7 @@ def parse_config( FileNotFoundError: Raised if the provided file is not found. Returns: - ConfigModel: Loaded and possibly completed pydantic config model. + dict: Loaded and completed config dict, possibly verified by pydantic config model. """ if config is None: config = {} diff --git a/sed/core/processor.py b/sed/core/processor.py index 0ea90517..1a7781f7 100644 --- a/sed/core/processor.py +++ b/sed/core/processor.py @@ -1167,11 +1167,11 @@ def load_bias_series( Args: binned_data (xr.DataArray | tuple[np.ndarray, np.ndarray, np.ndarray], optional): Binned data If provided as DataArray, Needs to contain dimensions - config["dataframe"]["tof_column"] and config["dataframe"]["bias_column"]. If - provided as tuple, needs to contain elements tof, biases, traces. + config["dataframe"]["columns"]["tof"] and config["dataframe"]["columns"]["bias"]. + If provided as tuple, needs to contain elements tof, biases, traces. data_files (list[str], optional): list of file paths to bin axes (list[str], optional): bin axes. - Defaults to config["dataframe"]["tof_column"]. + Defaults to config["dataframe"]["columns"]["tof"]. bins (list, optional): number of bins. Defaults to config["energy"]["bins"]. ranges (Sequence[tuple[float, float]], optional): bin ranges. @@ -1629,7 +1629,7 @@ def append_tof_ns_axis( Args: tof_ns_column (str, optional): Name of the generated column containing the time-of-flight in nanosecond. - Defaults to config["dataframe"]["tof_ns_column"]. + Defaults to config["dataframe"]["columns"]["tof_ns"]. preview (bool, optional): Option to preview the first elements of the data frame. Defaults to False. **kwds: additional arguments are passed to ``EnergyCalibrator.append_tof_ns_axis()``. diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index c9d73a4c..83d7c97c 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -686,7 +686,7 @@ def create_dataframe_per_file( with h5py.File(file_path, "r") as h5_file: self.reset_multi_index() # Reset MultiIndexes for next file df = self.concatenate_channels(h5_file) - df = df.dropna(subset=self._config["dataframe"].get("tof_column", "dldTimeSteps")) + df = df.dropna(subset=self._config["dataframe"]["columns"].get("tof", "dldTimeSteps")) # correct the 3 bit shift which encodes the detector ID in the 8s time if self._config["dataframe"].get("split_sector_id_from_dld_time", False): df, _ = split_dld_time_from_sector_id(df, config=self._config) @@ -763,7 +763,7 @@ def buffer_file_handler( parquet_schemas = [pq.read_schema(file) for file in existing_parquet_filenames] config_schema = set(self.get_channels(formats="all", index=True)) if self._config["dataframe"].get("split_sector_id_from_dld_time", False): - config_schema.add(self._config["dataframe"].get("sector_id_column", False)) + config_schema.add(self._config["dataframe"]["columns"].get("sector_id", False)) for i, schema in enumerate(parquet_schemas): schema_set = set(schema.names) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index db3aabbb..8c57924b 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -108,10 +108,8 @@ dataframe: sub_channels: sampleBias: slice: 0 - dtype: float32 tofVoltage: slice: 1 - dtype: float64 extractorVoltage: slice: 2 extractorCurrent: diff --git a/tests/test_config.py b/tests/test_config.py index df93520f..bdbdd9c8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -41,7 +41,7 @@ def test_default_config() -> None: """Test the config loader for the default config.""" - config = parse_config(verify_config=False) + config = parse_config(config={}, folder_config={}, user_config={}, system_config={}) assert isinstance(config, dict) for key in default_config_keys: assert key in config.keys() @@ -150,28 +150,86 @@ def test_save_dict() -> None: @pytest.mark.parametrize("config_path", config_paths) def test_config_model_valid(config_path) -> None: """Test the config model for a valid config.""" - config = parse_config(config_path, user_config={}, system_config={}, verify_config=True) + config = parse_config( + config_path, + folder_config={}, + user_config={}, + system_config={}, + verify_config=True, + ) assert config is not None def test_invalid_config_extra_field(): """Test that an invalid config with an extra field fails validation.""" - default_config = parse_config(user_config={}, system_config={}, verify_config=True) + default_config = parse_config( + folder_config={}, + user_config={}, + system_config={}, + verify_config=True, + ) invalid_config = default_config.copy() invalid_config["extra_field"] = "extra_value" with pytest.raises(ValidationError): - parse_config(invalid_config, user_config={}, system_config={}, verify_config=True) + parse_config( + invalid_config, + folder_config={}, + user_config={}, + system_config={}, + verify_config=True, + ) def test_invalid_config_missing_field(): """Test that an invalid config with a missing required field fails validation.""" - default_config = parse_config(user_config={}, system_config={}, verify_config=True) + default_config = parse_config( + folder_config={}, + user_config={}, + system_config={}, + verify_config=True, + ) invalid_config = default_config.copy() del invalid_config["core"]["loader"] with pytest.raises(ValidationError): parse_config( + folder_config={}, + user_config={}, + system_config={}, + default_config=invalid_config, + verify_config=True, + ) + + +def test_invalid_config_wrong_values(): + """Test that the validators for certain fields fails validation if not fulfilled.""" + default_config = parse_config( + folder_config={}, + user_config={}, + system_config={}, + verify_config=True, + ) + invalid_config = default_config.copy() + invalid_config["core"]["loader"] = "nonexistent" + with pytest.raises(ValidationError) as e: + parse_config( + folder_config={}, + user_config={}, + system_config={}, + default_config=invalid_config, + verify_config=True, + ) + assert "Invalid loader nonexistent. Available loaders are:" in str(e.value) + invalid_config = default_config.copy() + invalid_config["core"]["copy_tool"] = {} + invalid_config["core"]["copy_tool"]["source"] = "./" + invalid_config["core"]["copy_tool"]["dest"] = "./" + invalid_config["core"]["copy_tool"]["gid"] = 9999 + with pytest.raises(ValidationError) as e: + parse_config( + folder_config={}, user_config={}, system_config={}, default_config=invalid_config, verify_config=True, ) + assert "Invalid value 9999 for gid. Group not found." in str(e.value) From fea015a34ade75b5b47ad92ab1bf94d6b5c33e7d Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 22 Oct 2024 10:13:44 +0200 Subject: [PATCH 229/300] Move static (#511) * move static config to core * config model linting * test fixes --- sed/config/config_model.py | 15 +++-------- sed/config/flash_example_config.yaml | 32 +++++++++++------------- sed/config/sxp_example_config.yaml | 15 +++++------ sed/loader/flash/loader.py | 4 +-- sed/loader/sxp/loader.py | 6 ++--- tests/data/loader/flash/config.yaml | 33 +++++++++++-------------- tests/data/loader/sxp/config.yaml | 16 ++++++------ tests/loader/flash/test_flash_loader.py | 2 +- tests/loader/sxp/test_sxp_loader.py | 2 +- 9 files changed, 52 insertions(+), 73 deletions(-) diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 5a840f3b..9bdcdec1 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -61,6 +61,9 @@ class CoreModel(BaseModel): instrument: Optional[str] = None beamline: Optional[str] = None copy_tool: Optional[CopyToolModel] = None + stream_name_prefixes: Optional[dict] = None + stream_name_postfixes: Optional[dict] = None + beamtime_dir: Optional[dict] = None @field_validator("loader") @classmethod @@ -159,17 +162,6 @@ class HistogramModel(BaseModel): ranges: Sequence[tuple[float, float]] -class StaticModel(BaseModel): - """Static configuration settings that shouldn't be changed by users.""" - - # flash specific settings - model_config = ConfigDict(extra="forbid") - - stream_name_prefixes: Optional[dict] = None - stream_name_postfixes: Optional[dict] = None - beamtime_dir: Optional[dict] = None - - class EnergyModel(BaseModel): model_config = ConfigDict(extra="forbid") @@ -360,4 +352,3 @@ class ConfigModel(BaseModel): histogram: HistogramModel metadata: Optional[MetadataModel] = None nexus: Optional[NexusModel] = None - static: Optional[StaticModel] = None diff --git a/sed/config/flash_example_config.yaml b/sed/config/flash_example_config.yaml index 0f12402b..823a53fe 100644 --- a/sed/config/flash_example_config.yaml +++ b/sed/config/flash_example_config.yaml @@ -19,6 +19,19 @@ core: # raw: "" # # location of the intermediate parquet files. # processed: "" + # The prefixes of the stream names for different DAQ systems for parsing filenames + stream_name_prefixes: + pbd: "GMD_DATA_gmd_data" + pbd2: "FL2PhotDiag_pbd2_gmd_data" + fl1user1: "FLASH1_USER1_stream_2" + fl1user2: "FLASH1_USER2_stream_2" + fl1user3: "FLASH1_USER3_stream_2" + fl2user1: "FLASH2_USER1_stream_2" + fl2user2: "FLASH2_USER2_stream_2" + # The beamtime directories for different DAQ systems. + # (Not to be changed by user) + beamtime_dir: + pg2: "/asap3/flash/gpfs/pg2/" binning: # Histogram computation mode to use. @@ -204,21 +217,4 @@ dataframe: # nexus: # reader: "mpes" # definition: "NXmpes" -# input_files: ["NXmpes_config_HEXTOF_light.json"] - -# (Not to be changed by user) -static: - # The prefixes of the stream names for different DAQ systems for parsing filenames - stream_name_prefixes: - pbd: "GMD_DATA_gmd_data" - pbd2: "FL2PhotDiag_pbd2_gmd_data" - fl1user1: "FLASH1_USER1_stream_2" - fl1user2: "FLASH1_USER2_stream_2" - fl1user3: "FLASH1_USER3_stream_2" - fl2user1: "FLASH2_USER1_stream_2" - fl2user2: "FLASH2_USER2_stream_2" - - # The beamtime directories for different DAQ systems. - # (Not to be changed by user) - beamtime_dir: - pg2: "/asap3/flash/gpfs/pg2/" +# input_files: ["NXmpes_config_HEXTOF_light.json"] \ No newline at end of file diff --git a/sed/config/sxp_example_config.yaml b/sed/config/sxp_example_config.yaml index b9e4d298..9b48c28c 100644 --- a/sed/config/sxp_example_config.yaml +++ b/sed/config/sxp_example_config.yaml @@ -6,6 +6,12 @@ core: year: 202302 beamline: sxp instrument: sxp + stream_name_prefixes: + DA03: "RAW-R" + stream_name_postfixes: + DA03: "-DA03-" + beamtime_dir: + sxp: "/gpfs/exfel/exp/SXP/" paths: raw: "/path/to/data" # change this to a local directory where you want to store the parquet files @@ -102,12 +108,3 @@ dataframe: format: per_train dataset_key: "/CONTROL/SCS_ILH_LAS/MDL/OPTICALDELAY_PP800/actualPosition/value" index_key: "/INDEX/trainId" - -static: - stream_name_prefixes: - DA03: "RAW-R" - stream_name_postfixes: - DA03: "-DA03-" - - beamtime_dir: - sxp: "/gpfs/exfel/exp/SXP/" diff --git a/sed/loader/flash/loader.py b/sed/loader/flash/loader.py index f451783f..6c84fc34 100644 --- a/sed/loader/flash/loader.py +++ b/sed/loader/flash/loader.py @@ -110,7 +110,7 @@ def _initialize_dirs(self) -> None: ) from exc beamtime_dir = Path( - self._config["static"]["beamtime_dir"][self._config["core"]["beamline"]], + self._config["core"]["beamtime_dir"][self._config["core"]["beamline"]], ) beamtime_dir = beamtime_dir.joinpath(f"{year}/data/{beamtime_id}/") @@ -175,7 +175,7 @@ def get_files_from_run_id( # type: ignore[override] FileNotFoundError: If no files are found for the given run in the directory. """ # Define the stream name prefixes based on the data acquisition identifier - stream_name_prefixes = self._config["static"]["stream_name_prefixes"] + stream_name_prefixes = self._config["core"]["stream_name_prefixes"] if folders is None: folders = self._config["core"]["base_folder"] diff --git a/sed/loader/sxp/loader.py b/sed/loader/sxp/loader.py index 83d7c97c..9ff0ac9a 100644 --- a/sed/loader/sxp/loader.py +++ b/sed/loader/sxp/loader.py @@ -116,7 +116,7 @@ def _initialize_dirs(self): ) from exc beamtime_dir = Path( - self._config["static"]["beamtime_dir"][self._config["core"]["beamline"]], + self._config["core"]["beamtime_dir"][self._config["core"]["beamline"]], ) beamtime_dir = beamtime_dir.joinpath(f"{year}/{beamtime_id}/") @@ -158,8 +158,8 @@ def get_files_from_run_id( FileNotFoundError: If no files are found for the given run in the directory. """ # Define the stream name prefixes based on the data acquisition identifier - stream_name_prefixes = self._config["static"]["stream_name_prefixes"] - stream_name_postfixes = self._config["static"].get("stream_name_postfixes", {}) + stream_name_prefixes = self._config["core"]["stream_name_prefixes"] + stream_name_postfixes = self._config["core"].get("stream_name_postfixes", {}) if isinstance(run_id, (int, np.integer)): run_id = str(run_id).zfill(4) diff --git a/tests/data/loader/flash/config.yaml b/tests/data/loader/flash/config.yaml index 8c57924b..fbbcba25 100644 --- a/tests/data/loader/flash/config.yaml +++ b/tests/data/loader/flash/config.yaml @@ -17,6 +17,21 @@ core: # beamtime_id: xxxxxxxx # year: 20xx + # The prefixes of the stream names for different DAQ systems for parsing filenames + stream_name_prefixes: + pbd: "GMD_DATA_gmd_data" + pbd2: "FL2PhotDiag_pbd2_gmd_data" + fl1user1: "FLASH1_USER1_stream_2" + fl1user2: "FLASH1_USER2_stream_2" + fl1user3: "FLASH1_USER3_stream_2" + fl2user1: "FLASH2_USER1_stream_2" + fl2user2: "FLASH2_USER2_stream_2" + + # The beamtime directories for different DAQ systems. + # (Not to be changed by user) + beamtime_dir: + pg2: "/asap3/flash/gpfs/pg2/" + dataframe: # The name of the DAQ system to use. Necessary to resolve the filenames/paths. @@ -141,21 +156,3 @@ dataframe: index_key: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/index" dataset_key: "/FL1/Photon Diagnostic/GMD/Pulse resolved energy/energy tunnel/value" slice: 0 - - -# (Not to be changed by user) -static: - # The prefixes of the stream names for different DAQ systems for parsing filenames - stream_name_prefixes: - pbd: "GMD_DATA_gmd_data" - pbd2: "FL2PhotDiag_pbd2_gmd_data" - fl1user1: "FLASH1_USER1_stream_2" - fl1user2: "FLASH1_USER2_stream_2" - fl1user3: "FLASH1_USER3_stream_2" - fl2user1: "FLASH2_USER1_stream_2" - fl2user2: "FLASH2_USER2_stream_2" - - # The beamtime directories for different DAQ systems. - # (Not to be changed by user) - beamtime_dir: - pg2: "/asap3/flash/gpfs/pg2/" diff --git a/tests/data/loader/sxp/config.yaml b/tests/data/loader/sxp/config.yaml index c5680847..2f88bc50 100644 --- a/tests/data/loader/sxp/config.yaml +++ b/tests/data/loader/sxp/config.yaml @@ -5,6 +5,13 @@ core: raw: "tests/data/loader/sxp/" processed: "tests/data/loader/sxp/parquet" num_cores: 10 + stream_name_prefixes: + DA03: "RAW-R" + stream_name_postfixes: + DA03: "-DA03-" + + beamtime_dir: + sxp: "/GPFS/exfel/exp/SXP/" dataframe: ubid_offset: 0 @@ -82,12 +89,3 @@ dataframe: format: per_train dataset_key: "/CONTROL/SCS_ILH_LAS/MDL/OPTICALDELAY_PP800/actualPosition/value" index_key: "/INDEX/trainId" - -static: - stream_name_prefixes: - DA03: "RAW-R" - stream_name_postfixes: - DA03: "-DA03-" - - beamtime_dir: - sxp: "/GPFS/exfel/exp/SXP/" diff --git a/tests/loader/flash/test_flash_loader.py b/tests/loader/flash/test_flash_loader.py index 3d9a5170..de0bdf35 100644 --- a/tests/loader/flash/test_flash_loader.py +++ b/tests/loader/flash/test_flash_loader.py @@ -33,7 +33,7 @@ def test_initialize_dirs( config_["core"]["year"] = "2000" # Find base path of beamline from config. Here, we use pg2 - base_path = config_["static"]["beamtime_dir"]["pg2"] + base_path = config_["core"]["beamtime_dir"]["pg2"] expected_path = ( Path(base_path) / config_["core"]["year"] / "data" / config_["core"]["beamtime_id"] ) diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 85a31eac..1ee06d41 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -87,7 +87,7 @@ def test_initialize_dirs(config_file: dict, fs) -> None: config["core"]["year"] = "2000" # Find base path of beamline from config. - base_path = config["static"]["beamtime_dir"]["sxp"] + base_path = config["core"]["beamtime_dir"]["sxp"] expected_path = Path(base_path) / config["core"]["year"] / config["core"]["beamtime_id"] # Create expected paths expected_raw_path = expected_path / "raw" From 6d5c55ccf355702c7bb070e936b72480c39c6eee Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 22 Oct 2024 19:36:17 +0200 Subject: [PATCH 230/300] fix benchmarks --- benchmarks/benchmark_sed.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index ba181f42..f181dec5 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -54,7 +54,7 @@ def test_binning_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_1d"]: print(f"Updating targets for 'binning_1d' to {float(np.mean(result))}") @@ -78,7 +78,7 @@ def test_binning_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["binning_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["binning_4d"]: print(f"Updating targets for 'binning_4d' to {float(np.mean(result))}") @@ -95,6 +95,7 @@ def test_splinewarp() -> None: user_config={}, system_config={}, verbose=True, + verify_config=False, ) processor.apply_momentum_correction() timer = timeit.Timer( @@ -103,7 +104,7 @@ def test_splinewarp() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin + assert min(result) < targets["inv_dfield"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["inv_dfield"]: print(f"Updating targets for 'inv_dfield' to {float(np.mean(result))}") @@ -120,6 +121,7 @@ def test_workflow_1d() -> None: user_config={}, system_config={}, verbose=True, + verify_config=False, ) processor.dataframe["sampleBias"] = 16.7 processor.add_jitter() @@ -138,7 +140,7 @@ def test_workflow_1d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_1d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_1d"]: print(f"Updating targets for 'workflow_1d' to {float(np.mean(result))}") @@ -155,6 +157,7 @@ def test_workflow_4d() -> None: user_config={}, system_config={}, verbose=True, + verify_config=False, ) processor.dataframe["sampleBias"] = 16.7 processor.add_jitter() @@ -173,7 +176,7 @@ def test_workflow_4d() -> None: ) result = timer.repeat(5, number=1) print(result) - assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin + assert min(result) < targets["workflow_4d"] * 1.25 # allows 25% error margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets["workflow_4d"]: print(f"Updating targets for 'workflow_4d' to {float(np.mean(result))}") @@ -199,7 +202,7 @@ def test_loader_compute(loader: BaseLoader) -> None: ) result = timer.repeat(20, number=1) print(result) - assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin + assert min(result) < targets[f"loader_compute_{loader_name}"] * 1.25 # allows 25% margin # update targets if > 20% improvement occurs beyond old bestmark if np.mean(result) < 0.8 * targets[f"loader_compute_{loader_name}"]: print( From 47458dd5625928f2a156abc348e8771b3fc4314d Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 12 Nov 2024 15:28:22 +0100 Subject: [PATCH 231/300] update merged changes --- .github/workflows/documentation.yml | 7 - poetry.lock | 889 ++++++++++++++-------------- pyproject.toml | 3 +- sed/config/config_model.py | 2 + sed/config/sxp_example_config.yaml | 15 +- 5 files changed, 467 insertions(+), 449 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 2bdc8cee..dec7767a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -47,13 +47,11 @@ jobs: sudo wget https://github.com/jgm/pandoc/releases/download/3.1.8/pandoc-3.1.8-1-amd64.deb sudo dpkg -i pandoc-3.1.8-1-amd64.deb - # rm because hextof_workflow notebook can not run outside maxwell - name: copy tutorial files to docs run: | cp -r $GITHUB_WORKSPACE/tutorial $GITHUB_WORKSPACE/docs/ cp -r $GITHUB_WORKSPACE/sed/config $GITHUB_WORKSPACE/docs/sed - - name: download RAW data # if: steps.cache-primes.outputs.cache-hit != 'true' run: | @@ -66,11 +64,6 @@ jobs: poetry run python scripts/build_flash_parquets.py poetry run python scripts/build_sxp_parquets.py - # to be removed later. This theme doesn't support <3.9 python and our lock file contains 3.8 - - name: install pydata-sphinx-theme - run: | - poetry run pip install pydata-sphinx-theme - - name: Change version for develop build if: startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main' run: | diff --git a/poetry.lock b/poetry.lock index 18057d4b..6307fd11 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,23 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "accessible-pygments" +version = "0.0.5" +description = "A collection of accessible pygments styles" +optional = false +python-versions = ">=3.9" +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[package.dependencies] +pygments = ">=1.5" + +[package.extras] +dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] +tests = ["hypothesis", "pytest"] + [[package]] name = "aiofiles" version = "22.1.0" @@ -265,13 +283,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.10.7.0.32.46" +version = "0.2024.11.11.0.32.38" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" files = [ - {file = "astropy_iers_data-0.2024.10.7.0.32.46-py3-none-any.whl", hash = "sha256:4b5ce3e3c8ac17632a28a35f4b847d8a8db084509f5f0db3238140dbb5149eae"}, - {file = "astropy_iers_data-0.2024.10.7.0.32.46.tar.gz", hash = "sha256:349ff09294f03112d9391a5ad660135bf2fcef7de411a52c20bcf8598fb029d1"}, + {file = "astropy_iers_data-0.2024.11.11.0.32.38-py3-none-any.whl", hash = "sha256:b20140e8ef74a2b53eaf66e054dbb60d05f2aafca475fb0dc1f7c64a593f45e0"}, + {file = "astropy_iers_data-0.2024.11.11.0.32.38.tar.gz", hash = "sha256:13ae8b56150a2ff0853b77e4d00d09b6528e5ca8a9875ec1002139996d46c885"}, ] [package.extras] @@ -352,21 +370,20 @@ lxml = ["lxml"] [[package]] name = "bleach" -version = "6.1.0" +version = "6.2.0" description = "An easy safelist-based HTML-sanitizing tool." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, + {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, + {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, ] [package.dependencies] -six = ">=1.9.0" webencodings = "*" [package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] +css = ["tinycss2 (>=1.1.0,<1.5)"] [[package]] name = "bokeh" @@ -775,73 +792,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.6.2" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, - {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, - {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, - {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, - {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, - {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, - {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, - {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, - {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, - {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, - {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, - {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, - {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, - {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, - {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, - {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.dependencies] @@ -896,37 +913,37 @@ test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailu [[package]] name = "debugpy" -version = "1.8.7" +version = "1.8.8" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.7-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95fe04a573b8b22896c404365e03f4eda0ce0ba135b7667a1e57bd079793b96b"}, - {file = "debugpy-1.8.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:628a11f4b295ffb4141d8242a9bb52b77ad4a63a2ad19217a93be0f77f2c28c9"}, - {file = "debugpy-1.8.7-cp310-cp310-win32.whl", hash = "sha256:85ce9c1d0eebf622f86cc68618ad64bf66c4fc3197d88f74bb695a416837dd55"}, - {file = "debugpy-1.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:29e1571c276d643757ea126d014abda081eb5ea4c851628b33de0c2b6245b037"}, - {file = "debugpy-1.8.7-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:caf528ff9e7308b74a1749c183d6808ffbedbb9fb6af78b033c28974d9b8831f"}, - {file = "debugpy-1.8.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba1d078cf2e1e0b8402e6bda528bf8fda7ccd158c3dba6c012b7897747c41a0"}, - {file = "debugpy-1.8.7-cp311-cp311-win32.whl", hash = "sha256:171899588bcd412151e593bd40d9907133a7622cd6ecdbdb75f89d1551df13c2"}, - {file = "debugpy-1.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:6e1c4ffb0c79f66e89dfd97944f335880f0d50ad29525dc792785384923e2211"}, - {file = "debugpy-1.8.7-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:4d27d842311353ede0ad572600c62e4bcd74f458ee01ab0dd3a1a4457e7e3706"}, - {file = "debugpy-1.8.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2"}, - {file = "debugpy-1.8.7-cp312-cp312-win32.whl", hash = "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca"}, - {file = "debugpy-1.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39"}, - {file = "debugpy-1.8.7-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40"}, - {file = "debugpy-1.8.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7"}, - {file = "debugpy-1.8.7-cp313-cp313-win32.whl", hash = "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba"}, - {file = "debugpy-1.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa"}, - {file = "debugpy-1.8.7-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:4b908291a1d051ef3331484de8e959ef3e66f12b5e610c203b5b75d2725613a7"}, - {file = "debugpy-1.8.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da8df5b89a41f1fd31503b179d0a84a5fdb752dddd5b5388dbd1ae23cda31ce9"}, - {file = "debugpy-1.8.7-cp38-cp38-win32.whl", hash = "sha256:b12515e04720e9e5c2216cc7086d0edadf25d7ab7e3564ec8b4521cf111b4f8c"}, - {file = "debugpy-1.8.7-cp38-cp38-win_amd64.whl", hash = "sha256:93176e7672551cb5281577cdb62c63aadc87ec036f0c6a486f0ded337c504596"}, - {file = "debugpy-1.8.7-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:90d93e4f2db442f8222dec5ec55ccfc8005821028982f1968ebf551d32b28907"}, - {file = "debugpy-1.8.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6db2a370e2700557a976eaadb16243ec9c91bd46f1b3bb15376d7aaa7632c81"}, - {file = "debugpy-1.8.7-cp39-cp39-win32.whl", hash = "sha256:a6cf2510740e0c0b4a40330640e4b454f928c7b99b0c9dbf48b11efba08a8cda"}, - {file = "debugpy-1.8.7-cp39-cp39-win_amd64.whl", hash = "sha256:6a9d9d6d31846d8e34f52987ee0f1a904c7baa4912bf4843ab39dadf9b8f3e0d"}, - {file = "debugpy-1.8.7-py2.py3-none-any.whl", hash = "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae"}, - {file = "debugpy-1.8.7.zip", hash = "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e"}, + {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, + {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, + {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, + {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, + {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, + {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, + {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, + {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, + {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, + {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, + {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, + {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, + {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, + {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, + {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, + {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, + {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, + {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, + {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, + {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, + {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, + {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, + {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, + {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, + {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, + {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, ] [[package]] @@ -979,13 +996,13 @@ files = [ [[package]] name = "docutils" -version = "0.21.2" +version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.9" +python-versions = ">=3.7" files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] [[package]] @@ -1280,13 +1297,13 @@ test = ["blosc2 (>=2.5.1)", "blosc2-grok (>=0.2.2)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.2" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, + {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, ] [package.extras] @@ -1308,13 +1325,13 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "imageio" -version = "2.35.1" +version = "2.36.0" description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "imageio-2.35.1-py3-none-any.whl", hash = "sha256:6eb2e5244e7a16b85c10b5c2fe0f7bf961b40fcb9f1a9fd1bd1d2c2f8fb3cd65"}, - {file = "imageio-2.35.1.tar.gz", hash = "sha256:4952dfeef3c3947957f6d5dedb1f4ca31c6e509a476891062396834048aeed2a"}, + {file = "imageio-2.36.0-py3-none-any.whl", hash = "sha256:471f1eda55618ee44a3c9960911c35e647d9284c68f077e868df633398f137f0"}, + {file = "imageio-2.36.0.tar.gz", hash = "sha256:1c8f294db862c256e9562354d65aa54725b8dafed7f10f02bb3ec20ec1678850"}, ] [package.dependencies] @@ -1322,8 +1339,8 @@ numpy = "*" pillow = ">=8.3.2" [package.extras] -all-plugins = ["astropy", "av", "imageio-ffmpeg", "psutil", "tifffile"] -all-plugins-pypy = ["av", "imageio-ffmpeg", "psutil", "tifffile"] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "numpy (>2)", "pillow-heif", "psutil", "rawpy", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] build = ["wheel"] dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] @@ -1595,17 +1612,17 @@ files = [ [[package]] name = "json5" -version = "0.9.27" +version = "0.9.28" description = "A Python implementation of the JSON5 data format." optional = true python-versions = ">=3.8.0" files = [ - {file = "json5-0.9.27-py3-none-any.whl", hash = "sha256:17b43d78d3a6daeca4d7030e9bf22092dba29b1282cc2d0cfa56f6febee8dc93"}, - {file = "json5-0.9.27.tar.gz", hash = "sha256:5a19de4a6ca24ba664dc7d50307eb73ba9a16dea5d6bde85677ae85d3ed2d8e0"}, + {file = "json5-0.9.28-py3-none-any.whl", hash = "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df"}, + {file = "json5-0.9.28.tar.gz", hash = "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e"}, ] [package.extras] -dev = ["build (==1.2.1)", "coverage (==7.5.3)", "mypy (==1.10.0)", "pip (==24.1)", "pylint (==3.2.3)", "ruff (==0.5.1)", "twine (==5.1.1)", "uv (==0.2.13)"] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] [[package]] name = "jsonpointer" @@ -2345,72 +2362,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -3208,95 +3225,90 @@ test = ["pytest-astropy (>=0.10)"] [[package]] name = "pillow" -version = "10.4.0" +version = "11.0.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -3626,28 +3638,56 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydata-sphinx-theme" +version = "0.15.4" +description = "Bootstrap-based Sphinx theme from the PyData community" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6"}, + {file = "pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d"}, +] + +[package.dependencies] +accessible-pygments = "*" +Babel = "*" +beautifulsoup4 = "*" +docutils = "!=0.17.0" +packaging = "*" +pygments = ">=2.7" +sphinx = ">=5" +typing-extensions = "*" + +[package.extras] +a11y = ["pytest-playwright"] +dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] +doc = ["ablog (>=0.11.8)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (>=3.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] +i18n = ["Babel", "jinja2"] +test = ["pytest", "pytest-cov", "pytest-regressions", "sphinx[test]"] + [[package]] name = "pyerfa" -version = "2.0.1.4" +version = "2.0.1.5" description = "Python bindings for ERFA" optional = false python-versions = ">=3.9" files = [ - {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-win32.whl", hash = "sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa"}, - {file = "pyerfa-2.0.1.4-cp39-abi3-win_amd64.whl", hash = "sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40"}, - {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032"}, - {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068"}, - {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c"}, - {file = "pyerfa-2.0.1.4.tar.gz", hash = "sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b282d7c60c4c47cf629c484c17ac504fcb04abd7b3f4dfcf53ee042afc3a5944"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:be1aeb70390dd03a34faf96749d5cabc58437410b4aab7213c512323932427df"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0603e8e1b839327d586c8a627cdc634b795e18b007d84f0cda5500a0908254e"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e43c7194e3242083f2350b46c09fd4bf8ba1bcc0ebd1460b98fc47fe2389906"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:07b80cd70701f5d066b1ac8cce406682cfcd667a1186ec7d7ade597239a6021d"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-win32.whl", hash = "sha256:d30b9b0df588ed5467e529d851ea324a67239096dd44703125072fd11b351ea2"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-win_amd64.whl", hash = "sha256:66292d437dcf75925b694977aa06eb697126e7b86553e620371ed3e48b5e0ad0"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4991dee680ff36c87911d8faa4c7d1aa6278ad9b5e0d16158cf22fa7d74ba25c"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690e258294202c86f479e78e80fd235cd27bd717f7f60062fccc3dbd6ef0b1a9"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:171ce9676a448a7eb555f03aa19ad5c749dbced1ce4f9923e4d93443c4a9c612"}, + {file = "pyerfa-2.0.1.5.tar.gz", hash = "sha256:17d6b24fe4846c65d5e7d8c362dcb08199dc63b30a236aedd73875cc83e1f6c0"}, ] [package.dependencies] -numpy = ">=1.19" +numpy = ">=1.19.3" [package.extras] docs = ["sphinx-astropy (>=1.3)"] @@ -3655,13 +3695,13 @@ test = ["pytest", "pytest-doctestplus (>=0.7)"] [[package]] name = "pyfakefs" -version = "5.7.0" +version = "5.7.1" description = "pyfakefs implements a fake file system that mocks the Python file system modules." optional = false python-versions = ">=3.7" files = [ - {file = "pyfakefs-5.7.0-py3-none-any.whl", hash = "sha256:0ebf388a79fd937a31e761bf75b6caf7eae8b5d143493a00b18d5cd590d78356"}, - {file = "pyfakefs-5.7.0.tar.gz", hash = "sha256:9cbf0e4f22891e7db94bd4e764a95e91a9beda371cc11fdff3c537d88987696a"}, + {file = "pyfakefs-5.7.1-py3-none-any.whl", hash = "sha256:6503ffe7f401701cf974b502311f926da2b0657a72244a6ba36e985ceb3dd783"}, + {file = "pyfakefs-5.7.1.tar.gz", hash = "sha256:24774c632f3b67ea26fd56b08115ba7c339d5cd65655410bca8572d73a1ae9a4"}, ] [[package]] @@ -3737,13 +3777,13 @@ dev = ["mypy", "pip-tools", "pytest", "ruff (==0.5.5)", "types-pyyaml"] [[package]] name = "pyparsing" -version = "3.1.4" +version = "3.2.0" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.9" files = [ - {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, - {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, ] [package.extras] @@ -3788,17 +3828,17 @@ rich = ">=8.0.0" [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -3862,29 +3902,29 @@ files = [ [[package]] name = "pywin32" -version = "307" +version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-307-cp310-cp310-win32.whl", hash = "sha256:f8f25d893c1e1ce2d685ef6d0a481e87c6f510d0f3f117932781f412e0eba31b"}, - {file = "pywin32-307-cp310-cp310-win_amd64.whl", hash = "sha256:36e650c5e5e6b29b5d317385b02d20803ddbac5d1031e1f88d20d76676dd103d"}, - {file = "pywin32-307-cp310-cp310-win_arm64.whl", hash = "sha256:0c12d61e0274e0c62acee79e3e503c312426ddd0e8d4899c626cddc1cafe0ff4"}, - {file = "pywin32-307-cp311-cp311-win32.whl", hash = "sha256:fec5d27cc893178fab299de911b8e4d12c5954e1baf83e8a664311e56a272b75"}, - {file = "pywin32-307-cp311-cp311-win_amd64.whl", hash = "sha256:987a86971753ed7fdd52a7fb5747aba955b2c7fbbc3d8b76ec850358c1cc28c3"}, - {file = "pywin32-307-cp311-cp311-win_arm64.whl", hash = "sha256:fd436897c186a2e693cd0437386ed79f989f4d13d6f353f8787ecbb0ae719398"}, - {file = "pywin32-307-cp312-cp312-win32.whl", hash = "sha256:07649ec6b01712f36debf39fc94f3d696a46579e852f60157a729ac039df0815"}, - {file = "pywin32-307-cp312-cp312-win_amd64.whl", hash = "sha256:00d047992bb5dcf79f8b9b7c81f72e0130f9fe4b22df613f755ab1cc021d8347"}, - {file = "pywin32-307-cp312-cp312-win_arm64.whl", hash = "sha256:b53658acbfc6a8241d72cc09e9d1d666be4e6c99376bc59e26cdb6223c4554d2"}, - {file = "pywin32-307-cp313-cp313-win32.whl", hash = "sha256:ea4d56e48dc1ab2aa0a5e3c0741ad6e926529510516db7a3b6981a1ae74405e5"}, - {file = "pywin32-307-cp313-cp313-win_amd64.whl", hash = "sha256:576d09813eaf4c8168d0bfd66fb7cb3b15a61041cf41598c2db4a4583bf832d2"}, - {file = "pywin32-307-cp313-cp313-win_arm64.whl", hash = "sha256:b30c9bdbffda6a260beb2919f918daced23d32c79109412c2085cbc513338a0a"}, - {file = "pywin32-307-cp37-cp37m-win32.whl", hash = "sha256:5101472f5180c647d4525a0ed289ec723a26231550dbfd369ec19d5faf60e511"}, - {file = "pywin32-307-cp37-cp37m-win_amd64.whl", hash = "sha256:05de55a7c110478dc4b202230e98af5e0720855360d2b31a44bb4e296d795fba"}, - {file = "pywin32-307-cp38-cp38-win32.whl", hash = "sha256:13d059fb7f10792542082f5731d5d3d9645320fc38814759313e5ee97c3fac01"}, - {file = "pywin32-307-cp38-cp38-win_amd64.whl", hash = "sha256:7e0b2f93769d450a98ac7a31a087e07b126b6d571e8b4386a5762eb85325270b"}, - {file = "pywin32-307-cp39-cp39-win32.whl", hash = "sha256:55ee87f2f8c294e72ad9d4261ca423022310a6e79fb314a8ca76ab3f493854c6"}, - {file = "pywin32-307-cp39-cp39-win_amd64.whl", hash = "sha256:e9d5202922e74985b037c9ef46778335c102b74b95cec70f629453dbe7235d87"}, + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] @@ -4181,13 +4221,13 @@ files = [ [[package]] name = "rich" -version = "13.9.2" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, - {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] @@ -4200,114 +4240,101 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.20.1" +version = "0.21.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, - {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, - {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, - {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, - {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, - {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, - {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, - {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, - {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, - {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, - {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, - {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, - {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, - {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, - {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, + {file = "rpds_py-0.21.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590"}, + {file = "rpds_py-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5"}, + {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624"}, + {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664"}, + {file = "rpds_py-0.21.0-cp310-none-win32.whl", hash = "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682"}, + {file = "rpds_py-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5"}, + {file = "rpds_py-0.21.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95"}, + {file = "rpds_py-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d"}, + {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a"}, + {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8"}, + {file = "rpds_py-0.21.0-cp311-none-win32.whl", hash = "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a"}, + {file = "rpds_py-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e"}, + {file = "rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d"}, + {file = "rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf"}, + {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b"}, + {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11"}, + {file = "rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952"}, + {file = "rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd"}, + {file = "rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937"}, + {file = "rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94"}, + {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3"}, + {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976"}, + {file = "rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202"}, + {file = "rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e"}, + {file = "rpds_py-0.21.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928"}, + {file = "rpds_py-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592"}, + {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87"}, + {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed"}, + {file = "rpds_py-0.21.0-cp39-none-win32.whl", hash = "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8"}, + {file = "rpds_py-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c"}, + {file = "rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e"}, + {file = "rpds_py-0.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89"}, + {file = "rpds_py-0.21.0.tar.gz", hash = "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db"}, ] [[package]] @@ -4784,13 +4811,13 @@ test = ["pytest", "ruff"] [[package]] name = "tomli" -version = "2.0.2" +version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] @@ -4979,13 +5006,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.6" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, - {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] @@ -5010,19 +5037,15 @@ files = [ [[package]] name = "webcolors" -version = "24.8.0" +version = "24.11.1" description = "A library for working with the color formats defined by HTML and CSS." optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a"}, - {file = "webcolors-24.8.0.tar.gz", hash = "sha256:08b07af286a01bcd30d583a7acadf629583d1f79bfef27dd2c2c5c263817277d"}, + {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, + {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, ] -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["coverage[toml]"] - [[package]] name = "webencodings" version = "0.5.1" @@ -5203,13 +5226,13 @@ test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -5227,4 +5250,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "c2633c759cb833df4706cde9489249d5987be07180f9967ae1164b329f616224" +content-hash = "1f3253586eda248efed89d3a41d876407dfc9024117879799ca6aa7567a14a0e" diff --git a/pyproject.toml b/pyproject.toml index b590a703..4d71473e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "sed-processor" packages = [ {include = "sed"} ] -version = "0.3.1" +version = "1.0.0a0" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = ["OpenCOMPES team "] readme = "README.md" @@ -71,6 +71,7 @@ tomlkit = ">=0.12.0" sphinx-autodoc-typehints = ">=1.17.0" nbsphinx = ">=0.9.3" myst-parser = ">=2.0.0" +pydata-sphinx-theme = "^0.15.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/sed/config/config_model.py b/sed/config/config_model.py index 9bdcdec1..880e5864 100644 --- a/sed/config/config_model.py +++ b/sed/config/config_model.py @@ -107,6 +107,7 @@ class ChannelModel(BaseModel): dtype: Optional[str] = None max_hits: Optional[int] = None scale: Optional[float] = None + daq: Optional[str] = None class subChannel(BaseModel): model_config = ConfigDict(extra="forbid") @@ -142,6 +143,7 @@ class DataframeModel(BaseModel): daq: Optional[str] = None # SXP specific settings num_trains: Optional[PositiveInt] = None + num_pulses: Optional[PositiveInt] = None class BinningModel(BaseModel): diff --git a/sed/config/sxp_example_config.yaml b/sed/config/sxp_example_config.yaml index ccf855a0..832a45c1 100644 --- a/sed/config/sxp_example_config.yaml +++ b/sed/config/sxp_example_config.yaml @@ -34,7 +34,7 @@ dataframe: daq: DA03 forward_fill_iterations: 2 num_trains: 10 - num_pulses: 400 # only needed for data from new DAQ + # num_pulses: 400 # only needed for data from new DAQ tof_binwidth: 6.875E-12 # in seconds tof_binning: 1 jitter_cols: ["dldPosX", "dldPosY", "dldTimeSteps"] @@ -109,18 +109,17 @@ dataframe: format: per_train dataset_key: "/CONTROL/SCS_ILH_LAS/MDL/OPTICALDELAY_PP800/actualPosition/value" index_key: "/INDEX/trainId" - test: - daq: DA02 # change DAQ for a channel - format: per_pulse - dataset_key: "/INSTRUMENT/SA3_XTD10_XGM/XGM/DOOCS:output/data/intensitySa3TD" - index_key: "/INSTRUMENT/SA3_XTD10_XGM/XGM/DOOCS:output/data/trainId" + # test: + # daq: DA02 # change DAQ for a channel + # format: per_pulse + # dataset_key: "/INSTRUMENT/SA3_XTD10_XGM/XGM/DOOCS:output/data/intensitySa3TD" + # index_key: "/INSTRUMENT/SA3_XTD10_XGM/XGM/DOOCS:output/data/trainId" histogram: # number of bins used for histogram visualization bins: [80, 80, 80, 80] # default axes to use for histogram visualization. # Axes names starting with "@" refer to keys in the "dataframe" section - axes: ["@x_column", "@y_column", "@tof_column", "@delay_column"] + axes: ["@x", "@y", "@tof", "@delay"] # default ranges to use for histogram visualization (in unbinned detector coordinates) ranges: [[0, 4000], [0, 4000], [1000, 28000], [-1000, 1000]] - \ No newline at end of file From d26654b51ebc15cf6131aa1c27314f32527e5c17 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 12 Nov 2024 16:46:45 +0100 Subject: [PATCH 232/300] update build scripts to new config model --- docs/scripts/build_flash_parquets.py | 4 ++-- docs/scripts/build_sxp_parquets.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/scripts/build_flash_parquets.py b/docs/scripts/build_flash_parquets.py index d3e45c26..152df4de 100644 --- a/docs/scripts/build_flash_parquets.py +++ b/docs/scripts/build_flash_parquets.py @@ -13,8 +13,8 @@ config_override = { "core": { "paths": { - "data_raw_dir": data_path, - "data_parquet_dir": data_path + "/processed/", + "raw": data_path, + "processed": data_path + "/processed/", }, }, } diff --git a/docs/scripts/build_sxp_parquets.py b/docs/scripts/build_sxp_parquets.py index dd870148..8e0e419f 100644 --- a/docs/scripts/build_sxp_parquets.py +++ b/docs/scripts/build_sxp_parquets.py @@ -13,8 +13,8 @@ config_override = { "core": { "paths": { - "data_raw_dir": data_path, - "data_parquet_dir": data_path + "/processed/", + "raw": data_path, + "processed": data_path + "/processed/", }, }, } From d18934ce3e39be6718b5140bb25b4ef70ea785b0 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 14 Oct 2024 22:11:26 +0200 Subject: [PATCH 233/300] move code to src folder --- {sed/config => config}/NXmpes_config.json | 0 {sed/config => config}/default.yaml | 0 {sed/config => config}/flash_example_config.yaml | 0 {sed/config => config}/mpes_example_config.yaml | 0 {sed/config => config}/sxp_example_config.yaml | 0 pyproject.toml | 6 ++---- {sed => src/sed}/__init__.py | 0 {sed => src/sed}/binning/__init__.py | 0 {sed => src/sed}/binning/binning.py | 0 {sed => src/sed}/binning/numba_bin.py | 0 {sed => src/sed}/binning/utils.py | 0 {sed => src/sed}/calibrator/__init__.py | 0 {sed => src/sed}/calibrator/delay.py | 0 {sed => src/sed}/calibrator/energy.py | 0 {sed => src/sed}/calibrator/momentum.py | 0 {sed => src/sed}/core/__init__.py | 0 {sed => src/sed}/core/config.py | 0 {sed/config => src/sed/core}/config_model.py | 0 {sed => src/sed}/core/dfops.py | 0 {sed => src/sed}/core/logging.py | 0 {sed => src/sed}/core/metadata.py | 0 {sed => src/sed}/core/processor.py | 0 {sed => src/sed}/dataset/__init__.py | 0 {sed => src/sed}/dataset/dataset.py | 0 {sed => src/sed}/dataset/datasets.json | 0 {sed => src/sed}/diagnostics.py | 0 {sed => src/sed}/io/__init__.py | 0 {sed => src/sed}/io/hdf5.py | 0 {sed => src/sed}/io/nexus.py | 0 {sed => src/sed}/io/tiff.py | 0 {sed => src/sed}/loader/__init__.py | 0 {sed => src/sed}/loader/base/README.md | 0 {sed => src/sed}/loader/base/__init__.py | 0 {sed => src/sed}/loader/base/loader.py | 0 {sed => src/sed}/loader/flash/__init__.py | 0 {sed => src/sed}/loader/flash/buffer_handler.py | 0 {sed => src/sed}/loader/flash/dataframe.py | 0 {sed => src/sed}/loader/flash/instruments.py | 0 {sed => src/sed}/loader/flash/loader.py | 0 {sed => src/sed}/loader/flash/metadata.py | 0 {sed => src/sed}/loader/flash/utils.py | 0 {sed => src/sed}/loader/generic/__init__.py | 0 {sed => src/sed}/loader/generic/loader.py | 0 {sed => src/sed}/loader/loader_interface.py | 0 {sed => src/sed}/loader/mirrorutil.py | 0 {sed => src/sed}/loader/mpes/__init__.py | 0 {sed => src/sed}/loader/mpes/loader.py | 0 {sed => src/sed}/loader/sxp/__init__.py | 0 {sed => src/sed}/loader/sxp/loader.py | 0 {sed => src/sed}/loader/utils.py | 0 50 files changed, 2 insertions(+), 4 deletions(-) rename {sed/config => config}/NXmpes_config.json (100%) rename {sed/config => config}/default.yaml (100%) rename {sed/config => config}/flash_example_config.yaml (100%) rename {sed/config => config}/mpes_example_config.yaml (100%) rename {sed/config => config}/sxp_example_config.yaml (100%) rename {sed => src/sed}/__init__.py (100%) rename {sed => src/sed}/binning/__init__.py (100%) rename {sed => src/sed}/binning/binning.py (100%) rename {sed => src/sed}/binning/numba_bin.py (100%) rename {sed => src/sed}/binning/utils.py (100%) rename {sed => src/sed}/calibrator/__init__.py (100%) rename {sed => src/sed}/calibrator/delay.py (100%) rename {sed => src/sed}/calibrator/energy.py (100%) rename {sed => src/sed}/calibrator/momentum.py (100%) rename {sed => src/sed}/core/__init__.py (100%) rename {sed => src/sed}/core/config.py (100%) rename {sed/config => src/sed/core}/config_model.py (100%) rename {sed => src/sed}/core/dfops.py (100%) rename {sed => src/sed}/core/logging.py (100%) rename {sed => src/sed}/core/metadata.py (100%) rename {sed => src/sed}/core/processor.py (100%) rename {sed => src/sed}/dataset/__init__.py (100%) rename {sed => src/sed}/dataset/dataset.py (100%) rename {sed => src/sed}/dataset/datasets.json (100%) rename {sed => src/sed}/diagnostics.py (100%) rename {sed => src/sed}/io/__init__.py (100%) rename {sed => src/sed}/io/hdf5.py (100%) rename {sed => src/sed}/io/nexus.py (100%) rename {sed => src/sed}/io/tiff.py (100%) rename {sed => src/sed}/loader/__init__.py (100%) rename {sed => src/sed}/loader/base/README.md (100%) rename {sed => src/sed}/loader/base/__init__.py (100%) rename {sed => src/sed}/loader/base/loader.py (100%) rename {sed => src/sed}/loader/flash/__init__.py (100%) rename {sed => src/sed}/loader/flash/buffer_handler.py (100%) rename {sed => src/sed}/loader/flash/dataframe.py (100%) rename {sed => src/sed}/loader/flash/instruments.py (100%) rename {sed => src/sed}/loader/flash/loader.py (100%) rename {sed => src/sed}/loader/flash/metadata.py (100%) rename {sed => src/sed}/loader/flash/utils.py (100%) rename {sed => src/sed}/loader/generic/__init__.py (100%) rename {sed => src/sed}/loader/generic/loader.py (100%) rename {sed => src/sed}/loader/loader_interface.py (100%) rename {sed => src/sed}/loader/mirrorutil.py (100%) rename {sed => src/sed}/loader/mpes/__init__.py (100%) rename {sed => src/sed}/loader/mpes/loader.py (100%) rename {sed => src/sed}/loader/sxp/__init__.py (100%) rename {sed => src/sed}/loader/sxp/loader.py (100%) rename {sed => src/sed}/loader/utils.py (100%) diff --git a/sed/config/NXmpes_config.json b/config/NXmpes_config.json similarity index 100% rename from sed/config/NXmpes_config.json rename to config/NXmpes_config.json diff --git a/sed/config/default.yaml b/config/default.yaml similarity index 100% rename from sed/config/default.yaml rename to config/default.yaml diff --git a/sed/config/flash_example_config.yaml b/config/flash_example_config.yaml similarity index 100% rename from sed/config/flash_example_config.yaml rename to config/flash_example_config.yaml diff --git a/sed/config/mpes_example_config.yaml b/config/mpes_example_config.yaml similarity index 100% rename from sed/config/mpes_example_config.yaml rename to config/mpes_example_config.yaml diff --git a/sed/config/sxp_example_config.yaml b/config/sxp_example_config.yaml similarity index 100% rename from sed/config/sxp_example_config.yaml rename to config/sxp_example_config.yaml diff --git a/pyproject.toml b/pyproject.toml index 4d71473e..fd670628 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,6 @@ [tool.poetry] name = "sed-processor" -packages = [ - {include = "sed"} -] +packages = [{include = "sed", from = "src"}] version = "1.0.0a0" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = ["OpenCOMPES team "] @@ -84,7 +82,7 @@ omit = [ ] [tool.ruff] -include = ["sed/*.py", "tests/*.py"] +include = ["src/*.py", "tests/*.py"] lint.select = [ "E", # pycodestyle "W", # pycodestyle diff --git a/sed/__init__.py b/src/sed/__init__.py similarity index 100% rename from sed/__init__.py rename to src/sed/__init__.py diff --git a/sed/binning/__init__.py b/src/sed/binning/__init__.py similarity index 100% rename from sed/binning/__init__.py rename to src/sed/binning/__init__.py diff --git a/sed/binning/binning.py b/src/sed/binning/binning.py similarity index 100% rename from sed/binning/binning.py rename to src/sed/binning/binning.py diff --git a/sed/binning/numba_bin.py b/src/sed/binning/numba_bin.py similarity index 100% rename from sed/binning/numba_bin.py rename to src/sed/binning/numba_bin.py diff --git a/sed/binning/utils.py b/src/sed/binning/utils.py similarity index 100% rename from sed/binning/utils.py rename to src/sed/binning/utils.py diff --git a/sed/calibrator/__init__.py b/src/sed/calibrator/__init__.py similarity index 100% rename from sed/calibrator/__init__.py rename to src/sed/calibrator/__init__.py diff --git a/sed/calibrator/delay.py b/src/sed/calibrator/delay.py similarity index 100% rename from sed/calibrator/delay.py rename to src/sed/calibrator/delay.py diff --git a/sed/calibrator/energy.py b/src/sed/calibrator/energy.py similarity index 100% rename from sed/calibrator/energy.py rename to src/sed/calibrator/energy.py diff --git a/sed/calibrator/momentum.py b/src/sed/calibrator/momentum.py similarity index 100% rename from sed/calibrator/momentum.py rename to src/sed/calibrator/momentum.py diff --git a/sed/core/__init__.py b/src/sed/core/__init__.py similarity index 100% rename from sed/core/__init__.py rename to src/sed/core/__init__.py diff --git a/sed/core/config.py b/src/sed/core/config.py similarity index 100% rename from sed/core/config.py rename to src/sed/core/config.py diff --git a/sed/config/config_model.py b/src/sed/core/config_model.py similarity index 100% rename from sed/config/config_model.py rename to src/sed/core/config_model.py diff --git a/sed/core/dfops.py b/src/sed/core/dfops.py similarity index 100% rename from sed/core/dfops.py rename to src/sed/core/dfops.py diff --git a/sed/core/logging.py b/src/sed/core/logging.py similarity index 100% rename from sed/core/logging.py rename to src/sed/core/logging.py diff --git a/sed/core/metadata.py b/src/sed/core/metadata.py similarity index 100% rename from sed/core/metadata.py rename to src/sed/core/metadata.py diff --git a/sed/core/processor.py b/src/sed/core/processor.py similarity index 100% rename from sed/core/processor.py rename to src/sed/core/processor.py diff --git a/sed/dataset/__init__.py b/src/sed/dataset/__init__.py similarity index 100% rename from sed/dataset/__init__.py rename to src/sed/dataset/__init__.py diff --git a/sed/dataset/dataset.py b/src/sed/dataset/dataset.py similarity index 100% rename from sed/dataset/dataset.py rename to src/sed/dataset/dataset.py diff --git a/sed/dataset/datasets.json b/src/sed/dataset/datasets.json similarity index 100% rename from sed/dataset/datasets.json rename to src/sed/dataset/datasets.json diff --git a/sed/diagnostics.py b/src/sed/diagnostics.py similarity index 100% rename from sed/diagnostics.py rename to src/sed/diagnostics.py diff --git a/sed/io/__init__.py b/src/sed/io/__init__.py similarity index 100% rename from sed/io/__init__.py rename to src/sed/io/__init__.py diff --git a/sed/io/hdf5.py b/src/sed/io/hdf5.py similarity index 100% rename from sed/io/hdf5.py rename to src/sed/io/hdf5.py diff --git a/sed/io/nexus.py b/src/sed/io/nexus.py similarity index 100% rename from sed/io/nexus.py rename to src/sed/io/nexus.py diff --git a/sed/io/tiff.py b/src/sed/io/tiff.py similarity index 100% rename from sed/io/tiff.py rename to src/sed/io/tiff.py diff --git a/sed/loader/__init__.py b/src/sed/loader/__init__.py similarity index 100% rename from sed/loader/__init__.py rename to src/sed/loader/__init__.py diff --git a/sed/loader/base/README.md b/src/sed/loader/base/README.md similarity index 100% rename from sed/loader/base/README.md rename to src/sed/loader/base/README.md diff --git a/sed/loader/base/__init__.py b/src/sed/loader/base/__init__.py similarity index 100% rename from sed/loader/base/__init__.py rename to src/sed/loader/base/__init__.py diff --git a/sed/loader/base/loader.py b/src/sed/loader/base/loader.py similarity index 100% rename from sed/loader/base/loader.py rename to src/sed/loader/base/loader.py diff --git a/sed/loader/flash/__init__.py b/src/sed/loader/flash/__init__.py similarity index 100% rename from sed/loader/flash/__init__.py rename to src/sed/loader/flash/__init__.py diff --git a/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py similarity index 100% rename from sed/loader/flash/buffer_handler.py rename to src/sed/loader/flash/buffer_handler.py diff --git a/sed/loader/flash/dataframe.py b/src/sed/loader/flash/dataframe.py similarity index 100% rename from sed/loader/flash/dataframe.py rename to src/sed/loader/flash/dataframe.py diff --git a/sed/loader/flash/instruments.py b/src/sed/loader/flash/instruments.py similarity index 100% rename from sed/loader/flash/instruments.py rename to src/sed/loader/flash/instruments.py diff --git a/sed/loader/flash/loader.py b/src/sed/loader/flash/loader.py similarity index 100% rename from sed/loader/flash/loader.py rename to src/sed/loader/flash/loader.py diff --git a/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py similarity index 100% rename from sed/loader/flash/metadata.py rename to src/sed/loader/flash/metadata.py diff --git a/sed/loader/flash/utils.py b/src/sed/loader/flash/utils.py similarity index 100% rename from sed/loader/flash/utils.py rename to src/sed/loader/flash/utils.py diff --git a/sed/loader/generic/__init__.py b/src/sed/loader/generic/__init__.py similarity index 100% rename from sed/loader/generic/__init__.py rename to src/sed/loader/generic/__init__.py diff --git a/sed/loader/generic/loader.py b/src/sed/loader/generic/loader.py similarity index 100% rename from sed/loader/generic/loader.py rename to src/sed/loader/generic/loader.py diff --git a/sed/loader/loader_interface.py b/src/sed/loader/loader_interface.py similarity index 100% rename from sed/loader/loader_interface.py rename to src/sed/loader/loader_interface.py diff --git a/sed/loader/mirrorutil.py b/src/sed/loader/mirrorutil.py similarity index 100% rename from sed/loader/mirrorutil.py rename to src/sed/loader/mirrorutil.py diff --git a/sed/loader/mpes/__init__.py b/src/sed/loader/mpes/__init__.py similarity index 100% rename from sed/loader/mpes/__init__.py rename to src/sed/loader/mpes/__init__.py diff --git a/sed/loader/mpes/loader.py b/src/sed/loader/mpes/loader.py similarity index 100% rename from sed/loader/mpes/loader.py rename to src/sed/loader/mpes/loader.py diff --git a/sed/loader/sxp/__init__.py b/src/sed/loader/sxp/__init__.py similarity index 100% rename from sed/loader/sxp/__init__.py rename to src/sed/loader/sxp/__init__.py diff --git a/sed/loader/sxp/loader.py b/src/sed/loader/sxp/loader.py similarity index 100% rename from sed/loader/sxp/loader.py rename to src/sed/loader/sxp/loader.py diff --git a/sed/loader/utils.py b/src/sed/loader/utils.py similarity index 100% rename from sed/loader/utils.py rename to src/sed/loader/utils.py From 43756924bd2eda28c4d88b160be3991820f7f12e Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 14 Oct 2024 23:16:53 +0200 Subject: [PATCH 234/300] fix tests --- src/sed/core/config.py | 4 ++-- tests/calibrator/test_delay.py | 2 +- tests/calibrator/test_energy.py | 4 ++-- tests/calibrator/test_momentum.py | 4 ++-- tests/loader/flash/conftest.py | 11 ++++++----- tests/loader/mpes/test_mpes_loader.py | 2 +- tests/loader/sxp/test_sxp_loader.py | 2 +- tests/loader/test_loaders.py | 2 +- tests/loader/test_mirrorutil.py | 6 +++--- tests/test_config.py | 6 +++--- tests/test_dataset.py | 4 ++-- tests/test_diagnostics.py | 6 +++--- tests/test_processor.py | 14 +++++++------- 13 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/sed/core/config.py b/src/sed/core/config.py index ca29285c..d24b1b7b 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -12,7 +12,7 @@ import yaml from platformdirs import user_config_path -from sed.config.config_model import ConfigModel +from sed.core.config_model import ConfigModel from sed.core.logging import setup_logging package_dir = os.path.dirname(find_spec("sed").origin) @@ -28,7 +28,7 @@ def parse_config( folder_config: dict | str = None, user_config: dict | str = None, system_config: dict | str = None, - default_config: (dict | str) = f"{package_dir}/config/default.yaml", + default_config: (dict | str) = f"{package_dir}/../../config/default.yaml", verbose: bool = True, verify_config: bool = True, ) -> dict: diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index b5bbe49c..5b6838ed 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -16,7 +16,7 @@ from sed.loader.loader_interface import get_loader package_dir = os.path.dirname(find_spec("sed").origin) -file = package_dir + "/../tests/data/loader/mpes/Scan0030_2.h5" +file = package_dir + "/../../tests/data/loader/mpes/Scan0030_2.h5" def test_delay_parameters_from_file() -> None: diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index 21bac331..fdbda343 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -22,8 +22,8 @@ from sed.loader.loader_interface import get_loader package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../tests/data/loader/mpes/" -folder = package_dir + "/../tests/data/calibrator/" +df_folder = package_dir + "/../../tests/data/loader/mpes/" +folder = package_dir + "/../../tests/data/calibrator/" files = glob.glob(df_folder + "*.h5") traces_list = [] diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index de229e9f..a8e2f166 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -18,8 +18,8 @@ # pylint: disable=duplicate-code package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../tests/data/loader/mpes/" -folder = package_dir + "/../tests/data/calibrator/" +df_folder = package_dir + "/../../tests/data/loader/mpes/" +folder = package_dir + "/../../tests/data/calibrator/" files = glob.glob(df_folder + "*.h5") momentum_map_list = [] diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 8fa9c3b7..3cd98275 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -11,7 +11,7 @@ from sed.core.config import parse_config package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../tests/data/loader/flash/config.yaml") +config_path = os.path.join(package_dir, "../../tests/data/loader/flash/config.yaml") H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" H5_PATHS = [H5_PATH, "FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5"] @@ -45,7 +45,7 @@ def fixture_h5_file() -> h5py.File: Returns: h5py.File: The open h5 file. """ - return h5py.File(os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATH}"), "r") + return h5py.File(os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATH}"), "r") @pytest.fixture(name="h5_file_copy") @@ -56,7 +56,7 @@ def fixture_h5_file_copy(tmp_path: Path) -> h5py.File: h5py.File: The open h5 file copy. """ # Create a copy of the h5 file in a temporary directory - original_file_path = os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATH}") + original_file_path = os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATH}") copy_file_path = tmp_path / "copy.h5" shutil.copyfile(original_file_path, copy_file_path) @@ -72,7 +72,7 @@ def fixture_h5_file2_copy(tmp_path: Path) -> h5py.File: h5py.File: The open h5 file copy. """ # Create a copy of the h5 file in a temporary directory - original_file_path = os.path.join(package_dir, f"../tests/data/loader/flash/{H5_PATHS[1]}") + original_file_path = os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATHS[1]}") copy_file_path = tmp_path / "copy2.h5" shutil.copyfile(original_file_path, copy_file_path) @@ -88,5 +88,6 @@ def fixture_h5_paths() -> list[Path]: list: A list of h5 file paths. """ return [ - Path(os.path.join(package_dir, f"../tests/data/loader/flash/{path}")) for path in H5_PATHS + Path(os.path.join(package_dir, f"../../tests/data/loader/flash/{path}")) + for path in H5_PATHS ] diff --git a/tests/loader/mpes/test_mpes_loader.py b/tests/loader/mpes/test_mpes_loader.py index 936efe44..0bc15b6f 100644 --- a/tests/loader/mpes/test_mpes_loader.py +++ b/tests/loader/mpes/test_mpes_loader.py @@ -13,7 +13,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -test_data_dir = os.path.join(package_dir, "..", "tests", "data", "loader", "mpes") +test_data_dir = os.path.join(package_dir, "../../tests/data/loader/mpes") config = parse_config( os.path.join(test_data_dir, "config.yaml"), diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 1ee06d41..8ae3f64a 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -11,7 +11,7 @@ from sed.loader.sxp.loader import SXPLoader package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../tests/data/loader/sxp/config.yaml") +config_path = os.path.join(package_dir, "../../tests/data/loader/sxp/config.yaml") H5_PATH = "RAW-R0016-DA03-S00000.h5" diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index f3e0bf84..705f8ae8 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -21,7 +21,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -test_data_dir = os.path.join(package_dir, "..", "tests", "data") +test_data_dir = os.path.join(package_dir, "../../tests/data") read_types = ["one_file", "files", "one_folder", "folders", "one_run", "runs"] runs = {"generic": None, "mpes": ["30", "50"], "flash": ["43878", "43878"], "sxp": ["0016", "0016"]} diff --git a/tests/loader/test_mirrorutil.py b/tests/loader/test_mirrorutil.py index d7d522c8..5aa57477 100644 --- a/tests/loader/test_mirrorutil.py +++ b/tests/loader/test_mirrorutil.py @@ -16,9 +16,9 @@ package_dir = os.path.dirname(find_spec("sed").origin) -source_folder = package_dir + "/../" -folder = package_dir + "/../tests/data/loader/mpes" -file = package_dir + "/../tests/data/loader/mpes/Scan0030_2.h5" +source_folder = package_dir + "/../../" +folder = package_dir + "/../../tests/data/loader/mpes" +file = package_dir + "/../../tests/data/loader/mpes/Scan0030_2.h5" def test_copy_tool_folder() -> None: diff --git a/tests/test_config.py b/tests/test_config.py index bdbdd9c8..d94a44f1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -92,10 +92,10 @@ def test_load_does_not_modify() -> None: def test_load_config() -> None: """Test if the config loader can handle json and yaml files.""" config_json = load_config( - f"{package_dir}/../tests/data/config/config.json", + f"{package_dir}/../../tests/data/config/config.json", ) config_yaml = load_config( - f"{package_dir}/../tests/data/config/config.yaml", + f"{package_dir}/../../tests/data/config/config.yaml", ) assert config_json == config_yaml @@ -103,7 +103,7 @@ def test_load_config() -> None: def test_load_config_raise() -> None: """Test if the config loader raises an error for a wrong file type.""" with pytest.raises(TypeError): - load_config(f"{package_dir}/../README.md") + load_config(f"{package_dir}/../../README.md") def test_complete_dictionary() -> None: diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 4ff70ede..653b97e1 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -15,7 +15,7 @@ from sed.dataset import DatasetsManager as dm package_dir = os.path.dirname(find_spec("sed").origin) -json_path = os.path.join(package_dir, "datasets.json") +json_path = os.path.join(package_dir, "../../config/datasets.json") @pytest.fixture @@ -120,7 +120,7 @@ def test_rearrange_data(zip_file): # noqa: ARG001 ds._rearrange_data() assert os.path.exists("test/datasets/Test/test_file.txt") assert os.path.exists("test/datasets/Test/test_subdir.txt") - assert ~os.path.exists("test/datasets/Test/subdir") + assert not os.path.exists("test/datasets/Test/subdir") with pytest.raises(FileNotFoundError): ds._subdirs = ["non_existing_subdir"] diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index cb5bcce6..e99fdc1e 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -15,11 +15,11 @@ # pylint: disable=duplicate-code package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../tests/data/loader/mpes/" -folder = package_dir + "/../tests/data/calibrator/" +df_folder = package_dir + "/../../tests/data/loader/mpes/" +folder = package_dir + "/../../tests/data/calibrator/" files = glob.glob(df_folder + "*.h5") config = parse_config( - package_dir + "/../tests/data/loader/mpes/config.yaml", + package_dir + "/../../tests/data/loader/mpes/config.yaml", folder_config={}, user_config={}, system_config={}, diff --git a/tests/test_processor.py b/tests/test_processor.py index af204eec..ca489f38 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -26,14 +26,14 @@ # pylint: disable=duplicate-code package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../tests/data/loader/mpes/" -df_folder_generic = package_dir + "/../tests/data/loader/generic/" -folder = package_dir + "/../tests/data/calibrator/" +df_folder = package_dir + "/../../tests/data/loader/mpes/" +df_folder_generic = package_dir + "/../../tests/data/loader/generic/" +folder = package_dir + "/../../tests/data/calibrator/" files = glob.glob(df_folder + "*.h5") runs = ["30", "50"] runs_flash = ["43878", "43878"] loader = get_loader(loader_name="mpes") -source_folder = package_dir + "/../" +source_folder = package_dir + "/../../" dest_folder = tempfile.mkdtemp() gid = os.getgid() @@ -852,7 +852,7 @@ def test_add_time_stamped_data() -> None: """Test the function to add time-stamped data""" processor = SedProcessor( folder=df_folder + "../mpes/", - config=package_dir + "/config/mpes_example_config.yaml", + config=package_dir + "/../../config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -1083,7 +1083,7 @@ def test_save(caplog) -> None: config = parse_config( config={"dataframe": {"tof_binning": 1}}, folder_config={}, - user_config=package_dir + "/../sed/config/mpes_example_config.yaml", + user_config=package_dir + "/../../config/mpes_example_config.yaml", system_config={}, verify_config=False, ) @@ -1140,7 +1140,7 @@ def test_save(caplog) -> None: yaml.dump({"Instrument": {"undocumented_field": "undocumented entry"}}, f) with open("temp_config.json", "w") as f: with open( - package_dir + "/../sed/config/NXmpes_config.json", + package_dir + "/../../config/NXmpes_config.json", encoding="utf-8", ) as stream: config_dict = json.load(stream) From f2931ef46ec198f01ce39ab78335fdf06d1ef2fc Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 14 Oct 2024 23:25:29 +0200 Subject: [PATCH 235/300] update workflows --- .github/workflows/documentation.yml | 4 ++-- .github/workflows/linting.yml | 6 +++--- .github/workflows/release.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index dec7767a..6afef727 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -5,7 +5,7 @@ on: branches: [ main ] tags: [ v* ] paths: - - sed/**/* + - src/sed/**/* - tutorial/** - .github/workflows/documentation.yml # Allows you to run this workflow manually from the Actions tab @@ -50,7 +50,7 @@ jobs: - name: copy tutorial files to docs run: | cp -r $GITHUB_WORKSPACE/tutorial $GITHUB_WORKSPACE/docs/ - cp -r $GITHUB_WORKSPACE/sed/config $GITHUB_WORKSPACE/docs/sed + cp -r $GITHUB_WORKSPACE/config $GITHUB_WORKSPACE/docs/ - name: download RAW data # if: steps.cache-primes.outputs.cache-hit != 'true' diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 593077cc..89c92048 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -25,15 +25,15 @@ jobs: # Linting steps, execute all linters even if one fails - name: ruff run: - poetry run ruff sed tests + poetry run ruff src/sed tests - name: ruff formatting if: ${{ always() }} run: - poetry run ruff format --check sed tests + poetry run ruff format --check src/sed tests - name: mypy if: ${{ always() }} run: - poetry run mypy sed tests + poetry run mypy src/sed tests - name: spellcheck if: ${{ always() }} uses: streetsidesoftware/cspell-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aed41825..ce492666 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ on: tags: - v[0-9]+.[0-9]+.[0-9]+ paths: - - sed/**/* + - src/sed/**/* - .github/workflows/release.yml # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 35e49660ad498beaafa3c25d785a9ec85e7184e6 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 14 Oct 2024 23:56:09 +0200 Subject: [PATCH 236/300] update tutorials --- {src/sed/dataset => config}/datasets.json | 0 src/sed/dataset/dataset.py | 2 +- ...nversion_pipeline_for_example_time-resolved_ARPES_data.ipynb | 2 +- tutorial/3_metadata_collection_and_export_to_NeXus.ipynb | 2 +- tutorial/4_hextof_workflow.ipynb | 2 +- tutorial/5_sxp_workflow.ipynb | 2 +- tutorial/6_binning_with_time-stamped_data.ipynb | 2 +- tutorial/7_correcting_orthorhombic_symmetry.ipynb | 2 +- tutorial/8_jittering_tutorial.ipynb | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename {src/sed/dataset => config}/datasets.json (100%) diff --git a/src/sed/dataset/datasets.json b/config/datasets.json similarity index 100% rename from src/sed/dataset/datasets.json rename to config/datasets.json diff --git a/src/sed/dataset/dataset.py b/src/sed/dataset/dataset.py index fca7fc83..ad445924 100644 --- a/src/sed/dataset/dataset.py +++ b/src/sed/dataset/dataset.py @@ -32,7 +32,7 @@ class DatasetsManager: FILENAME = NAME + ".json" json_path = {} json_path["user"] = os.path.join(USER_CONFIG_PATH, FILENAME) - json_path["module"] = os.path.join(os.path.dirname(__file__), FILENAME) + json_path["module"] = os.path.join(os.path.dirname(__file__), "../../../config", FILENAME) json_path["folder"] = "./" + FILENAME @staticmethod diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 19fb3b74..2a10dbba 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -59,7 +59,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={}, verbose=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={}, verbose=True)" ] }, { diff --git a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb index 7ac5d39f..53b58a05 100644 --- a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb +++ b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb @@ -143,7 +143,7 @@ "outputs": [], "source": [ "# create sed processor using the config file, and collect the meta data from the files:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={}, metadata=metadata, collect_metadata=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={}, metadata=metadata, collect_metadata=True)" ] }, { diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 273e09e1..ad3d2e5c 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -103,7 +103,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../sed/config/flash_example_config.yaml')\n", + "config_file = Path('../config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index 713cef9c..b225a96e 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -85,7 +85,7 @@ "outputs": [], "source": [ "# pick the default configuration file for SXP@XFEL\n", - "config_file = Path('../sed/config/sxp_example_config.yaml')\n", + "config_file = Path('../config/sxp_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index 7978568f..20b9e335 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)" + "sp = sed.SedProcessor(folder=scandir, user_config=\"../config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)" ] }, { diff --git a/tutorial/7_correcting_orthorhombic_symmetry.ipynb b/tutorial/7_correcting_orthorhombic_symmetry.ipynb index faba30b6..47bd1530 100644 --- a/tutorial/7_correcting_orthorhombic_symmetry.ipynb +++ b/tutorial/7_correcting_orthorhombic_symmetry.ipynb @@ -60,7 +60,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)\n", + "sp = sed.SedProcessor(folder=scandir, user_config=\"../config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)\n", "sp.add_jitter()" ] }, diff --git a/tutorial/8_jittering_tutorial.ipynb b/tutorial/8_jittering_tutorial.ipynb index d98a19b2..5b94dd08 100644 --- a/tutorial/8_jittering_tutorial.ipynb +++ b/tutorial/8_jittering_tutorial.ipynb @@ -58,7 +58,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../sed/config/mpes_example_config.yaml\", system_config={})" + "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={})" ] }, { From 07cbc896316e65f51772b9ec839a070e033dd584 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 00:04:40 +0200 Subject: [PATCH 237/300] add config to build specs --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fd670628..f579cce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [tool.poetry] name = "sed-processor" packages = [{include = "sed", from = "src"}] +include = ["config"] version = "1.0.0a0" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = ["OpenCOMPES team "] From 757e19ef60c0e7f48dbff57ef6a7a2f03fe9d279 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 12:36:30 +0200 Subject: [PATCH 238/300] fix flash build script again --- docs/scripts/build_flash_parquets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/scripts/build_flash_parquets.py b/docs/scripts/build_flash_parquets.py index 152df4de..0200bfd1 100644 --- a/docs/scripts/build_flash_parquets.py +++ b/docs/scripts/build_flash_parquets.py @@ -1,10 +1,12 @@ -from pathlib import Path +import os +from importlib.util import find_spec -import sed from sed import SedProcessor from sed.dataset import dataset -config_file = Path(sed.__file__).parent / "config/flash_example_config.yaml" +package_dir = os.path.dirname(find_spec("sed").origin) + +config_file = package_dir + "/../../config/flash_example_config.yaml" dataset.get("Gd_W110", root_dir="./tutorial") data_path = dataset.dir From 1499550b6b4aea41a135e74b9928040cc64f406a Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 15:54:39 +0200 Subject: [PATCH 239/300] update path to Nexus config file --- config/mpes_example_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/mpes_example_config.yaml b/config/mpes_example_config.yaml index c45ca865..fe81c28f 100644 --- a/config/mpes_example_config.yaml +++ b/config/mpes_example_config.yaml @@ -310,4 +310,4 @@ nexus: definition: "NXmpes" # List containing additional input files to be handed to the pynxtools converter tool, # e.g. containing a configuration file, and additional metadata. - input_files: ["../sed/config/NXmpes_config.json"] + input_files: ["../config/NXmpes_config.json"] From a1010310fed622d410059e7255b57d2fe5de5719 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 16:38:11 +0200 Subject: [PATCH 240/300] fix tests --- tests/test_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_processor.py b/tests/test_processor.py index ca489f38..18a42756 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -1092,7 +1092,7 @@ def test_save(caplog) -> None: ] = 21.0 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["Z1"] = 2450 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["F"] = 69.23 - config["nexus"]["input_files"] = [package_dir + "/../sed/config/NXmpes_config.json"] + config["nexus"]["input_files"] = [package_dir + "/../../config/NXmpes_config.json"] processor = SedProcessor( folder=df_folder, config=config, From 0d1b805dee0d232ea333f6e78d354ccb3318d396 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 22 Oct 2024 20:24:48 +0200 Subject: [PATCH 241/300] fix benchmarks --- benchmarks/benchmark_sed.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index f181dec5..af016608 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -35,7 +35,7 @@ test_data_dir = os.path.join(package_dir, "..", "tests", "data") runs = {"generic": None, "mpes": ["30", "50"], "flash": ["43878"], "sxp": ["0016"]} -targets = load_config(package_dir + "/../benchmarks/benchmark_targets.yaml") +targets = load_config(package_dir + "/../../benchmarks/benchmark_targets.yaml") def test_binning_1d() -> None: @@ -59,7 +59,7 @@ def test_binning_1d() -> None: if np.mean(result) < 0.8 * targets["binning_1d"]: print(f"Updating targets for 'binning_1d' to {float(np.mean(result))}") targets["binning_1d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") def test_binning_4d() -> None: @@ -83,14 +83,14 @@ def test_binning_4d() -> None: if np.mean(result) < 0.8 * targets["binning_4d"]: print(f"Updating targets for 'binning_4d' to {float(np.mean(result))}") targets["binning_4d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") def test_splinewarp() -> None: """Run a benchmark for the generation of the inverse dfield correction""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/config/mpes_example_config.yaml", + config=package_dir + "/../../config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -109,14 +109,14 @@ def test_splinewarp() -> None: if np.mean(result) < 0.8 * targets["inv_dfield"]: print(f"Updating targets for 'inv_dfield' to {float(np.mean(result))}") targets["inv_dfield"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") def test_workflow_1d() -> None: """Run a benchmark for 1d binning of converted data""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/config/mpes_example_config.yaml", + config=package_dir + "/../../config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -145,14 +145,14 @@ def test_workflow_1d() -> None: if np.mean(result) < 0.8 * targets["workflow_1d"]: print(f"Updating targets for 'workflow_1d' to {float(np.mean(result))}") targets["workflow_1d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") def test_workflow_4d() -> None: """Run a benchmark for 4d binning of converted data""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/config/mpes_example_config.yaml", + config=package_dir + "/../../config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -181,7 +181,7 @@ def test_workflow_4d() -> None: if np.mean(result) < 0.8 * targets["workflow_4d"]: print(f"Updating targets for 'workflow_4d' to {float(np.mean(result))}") targets["workflow_4d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") @pytest.mark.parametrize("loader", get_all_loaders()) @@ -210,4 +210,4 @@ def test_loader_compute(loader: BaseLoader) -> None: f"to {float(np.mean(result))}", ) targets[f"loader_compute_{loader_name}"] = float(np.mean(result)) - save_config(targets, package_dir + "/../benchmarks/benchmark_targets.yaml") + save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") From df8d89b9d971b0913b8704e6ad274544af9226be Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 12 Nov 2024 23:28:51 +0100 Subject: [PATCH 242/300] move Flash Nxmpes config --- {sed/config => config}/NXmpes_config-HEXTOF.json | 0 config/flash_example_config.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {sed/config => config}/NXmpes_config-HEXTOF.json (100%) diff --git a/sed/config/NXmpes_config-HEXTOF.json b/config/NXmpes_config-HEXTOF.json similarity index 100% rename from sed/config/NXmpes_config-HEXTOF.json rename to config/NXmpes_config-HEXTOF.json diff --git a/config/flash_example_config.yaml b/config/flash_example_config.yaml index 4e3d2b60..c9168a98 100644 --- a/config/flash_example_config.yaml +++ b/config/flash_example_config.yaml @@ -218,4 +218,4 @@ dataframe: nexus: reader: "mpes" definition: "NXmpes" - input_files: ["../sed/config/NXmpes_config-HEXTOF.json"] + input_files: ["../config/NXmpes_config-HEXTOF.json"] From 49b4b028acb303be0ccb2d83211d35b8f7c92bb3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 12 Nov 2024 23:36:27 +0100 Subject: [PATCH 243/300] update build script --- docs/scripts/build_sxp_parquets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/scripts/build_sxp_parquets.py b/docs/scripts/build_sxp_parquets.py index 8e0e419f..69915e3f 100644 --- a/docs/scripts/build_sxp_parquets.py +++ b/docs/scripts/build_sxp_parquets.py @@ -1,10 +1,12 @@ -from pathlib import Path +import os +from importlib.util import find_spec -import sed from sed import SedProcessor from sed.dataset import dataset -config_file = Path(sed.__file__).parent / "config/sxp_example_config.yaml" +package_dir = os.path.dirname(find_spec("sed").origin) + +config_file = package_dir + "/../../config/sxp_example_config.yaml" dataset.get("Au_Mica", root_dir="./tutorial") data_path = dataset.dir From f3da0862c9aceae49ae75a0e5a5a84a8b5713109 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 12 Nov 2024 23:58:09 +0100 Subject: [PATCH 244/300] update jupyter to >v4 --- poetry.lock | 599 +++++++++++++++++++++---------------------------- pyproject.toml | 9 +- 2 files changed, 266 insertions(+), 342 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6307fd11..1ee7af0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -18,35 +18,6 @@ pygments = ">=1.5" dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] tests = ["hypothesis", "pytest"] -[[package]] -name = "aiofiles" -version = "22.1.0" -description = "File support for asyncio." -optional = true -python-versions = ">=3.7,<4.0" -files = [ - {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"}, - {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"}, -] - -[[package]] -name = "aiosqlite" -version = "0.20.0" -description = "asyncio bridge to the standard sqlite3 module" -optional = true -python-versions = ">=3.8" -files = [ - {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, - {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, -] - -[package.dependencies] -typing_extensions = ">=4.0" - -[package.extras] -dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] -docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] - [[package]] name = "alabaster" version = "0.7.16" @@ -71,24 +42,25 @@ files = [ [[package]] name = "anyio" -version = "3.7.1" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "anytree" @@ -108,7 +80,7 @@ six = "*" name = "appnope" version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, @@ -176,7 +148,7 @@ tests = ["pytest"] name = "arrow" version = "1.3.0" description = "Better dates & times for Python" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, @@ -314,6 +286,20 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = true +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + [[package]] name = "attrs" version = "24.2.0" @@ -915,7 +901,7 @@ test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailu name = "debugpy" version = "1.8.8" description = "An implementation of the Debug Adapter Protocol for Python" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, @@ -996,13 +982,13 @@ files = [ [[package]] name = "docutils" -version = "0.20.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -1166,7 +1152,7 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] name = "fqdn" version = "1.5.1" description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -optional = true +optional = false python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" files = [ {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, @@ -1212,26 +1198,38 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe, test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = true +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "h5grove" -version = "1.3.0" +version = "2.3.0" description = "Core utilities to serve HDF5 file contents" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "h5grove-1.3.0-py3-none-any.whl", hash = "sha256:340d7b47e9957bd666b92712d62d0841797449774a738ec8f519ed80ba9a13fb"}, - {file = "h5grove-1.3.0.tar.gz", hash = "sha256:e8f052ff497f0ff42477a24511bbf0d8a1cf0b6e7aea31957bdec7b4baae2c9a"}, + {file = "h5grove-2.3.0-py3-none-any.whl", hash = "sha256:7000a5aa64a6d77997ad18296552ec4def62d4e24aa05219b59c535233017fdf"}, + {file = "h5grove-2.3.0.tar.gz", hash = "sha256:8d438f2a4616d64b176e6ff352d23b39027b9f1ab2c9169fcfd6f5f1caca8728"}, ] [package.dependencies] -h5py = ">=2.9" +h5py = ">=3" numpy = "*" orjson = "*" tifffile = "*" +typing-extensions = "*" [package.extras] -dev = ["black", "bump2version", "check-manifest", "flake8", "h5grove[fastapi]", "h5grove[flask]", "h5grove[tornado]", "httpx (>=0.23)", "invoke", "mypy", "myst-parser", "pytest", "pytest-benchmark", "pytest-cov", "pytest-tornado", "sphinx", "sphinx-argparse", "types-contextvars", "types-dataclasses", "types-orjson", "types-pkg-resources"] -fastapi = ["fastapi", "uvicorn"] +dev = ["black", "bump2version", "check-manifest", "eval-type-backport", "flake8", "h5grove[fastapi]", "h5grove[flask]", "h5grove[tornado]", "httpx (>=0.23)", "invoke", "mypy", "myst-parser", "pytest", "pytest-benchmark", "pytest-cov", "pytest-tornado", "sphinx", "sphinx-argparse", "sphinx-autobuild", "types-contextvars", "types-dataclasses", "types-orjson", "types-setuptools"] +fastapi = ["fastapi", "pydantic (>2)", "pydantic-settings", "uvicorn"] flask = ["Flask", "Flask-Compress", "Flask-Cors"] tornado = ["tornado"] @@ -1295,6 +1293,52 @@ h5py = ">=3.0.0" doc = ["ipython", "nbsphinx", "sphinx", "sphinx-rtd-theme"] test = ["blosc2 (>=2.5.1)", "blosc2-grok (>=0.2.2)"] +[[package]] +name = "httpcore" +version = "1.0.6" +description = "A minimal low-level HTTP client." +optional = true +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, + {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = true +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.2" @@ -1427,7 +1471,7 @@ files = [ name = "ipykernel" version = "6.29.5" description = "IPython Kernel for Jupyter" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, @@ -1529,31 +1573,30 @@ files = [ [[package]] name = "ipywidgets" -version = "7.8.5" -description = "IPython HTML widgets for Jupyter" +version = "8.1.5" +description = "Jupyter interactive widgets" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "ipywidgets-7.8.5-py2.py3-none-any.whl", hash = "sha256:8055fe314edd4c101a5f1ea230620ef5e315b0ca87f940264b4eac1faf9746ef"}, - {file = "ipywidgets-7.8.5.tar.gz", hash = "sha256:927439399d75f59f43864c13d7e73b05a4de522d3ea09d6048adc5c583b55c3b"}, + {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, + {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, ] [package.dependencies] comm = ">=0.1.3" -ipython = {version = ">=4.0.0", markers = "python_version >= \"3.3\""} -ipython-genutils = ">=0.2.0,<0.3.0" -jupyterlab-widgets = {version = ">=1.0.0,<3", markers = "python_version >= \"3.6\""} +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.12,<3.1.0" traitlets = ">=4.3.1" -widgetsnbextension = ">=3.6.10,<3.7.0" +widgetsnbextension = ">=4.0.12,<4.1.0" [package.extras] -test = ["ipykernel", "mock", "pytest (>=3.6.0)", "pytest-cov"] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] [[package]] name = "isoduration" version = "20.11.0" description = "Operations with ISO 8601 durations" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, @@ -1628,7 +1671,7 @@ dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (= name = "jsonpointer" version = "3.0.0" description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, @@ -1768,7 +1811,7 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" name = "jupyter-events" version = "0.10.0" description = "Jupyter Event System library" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, @@ -1790,139 +1833,126 @@ docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontr test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] [[package]] -name = "jupyter-server" -version = "1.24.0" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyter_server-1.24.0-py3-none-any.whl", hash = "sha256:c88ddbe862966ea1aea8c3ccb89a5903abd8fbcfe5cd14090ef549d403332c37"}, - {file = "jupyter_server-1.24.0.tar.gz", hash = "sha256:23368e8e214baf82b313d4c5a0d828ca73015e1a192ce3829bd74e62fab8d046"}, -] - -[package.dependencies] -anyio = ">=3.1.0,<4" -argon2-cffi = "*" -jinja2 = "*" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbconvert = ">=6.4.4" -nbformat = ">=5.2.0" -packaging = "*" -prometheus-client = "*" -pywinpty = {version = "*", markers = "os_name == \"nt\""} -pyzmq = ">=17" -Send2Trash = "*" -terminado = ">=0.8.3" -tornado = ">=6.1.0" -traitlets = ">=5.1" -websocket-client = "*" - -[package.extras] -test = ["coverage", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-cov", "pytest-mock", "pytest-timeout", "pytest-tornasync", "requests"] - -[[package]] -name = "jupyter-server-fileid" -version = "0.9.3" -description = "Jupyter Server extension providing an implementation of the File ID service." +name = "jupyter-lsp" +version = "2.2.5" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jupyter_server_fileid-0.9.3-py3-none-any.whl", hash = "sha256:f73c01c19f90005d3fff93607b91b4955ba4e1dccdde9bfe8026646f94053791"}, - {file = "jupyter_server_fileid-0.9.3.tar.gz", hash = "sha256:521608bb87f606a8637fcbdce2f3d24a8b3cc89d2eef61751cb40e468d4e54be"}, + {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, + {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, ] [package.dependencies] -jupyter-events = ">=0.5.0" -jupyter-server = ">=1.15,<3" - -[package.extras] -cli = ["click"] -test = ["jupyter-server[test] (>=1.15,<3)", "pytest", "pytest-cov", "pytest-jupyter"] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-server = ">=1.1.2" [[package]] -name = "jupyter-server-ydoc" -version = "0.8.0" -description = "A Jupyter Server Extension Providing Y Documents." -optional = true -python-versions = ">=3.7" +name = "jupyter-server" +version = "2.14.2" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" files = [ - {file = "jupyter_server_ydoc-0.8.0-py3-none-any.whl", hash = "sha256:969a3a1a77ed4e99487d60a74048dc9fa7d3b0dcd32e60885d835bbf7ba7be11"}, - {file = "jupyter_server_ydoc-0.8.0.tar.gz", hash = "sha256:a6fe125091792d16c962cc3720c950c2b87fcc8c3ecf0c54c84e9a20b814526c"}, + {file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"}, + {file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"}, ] [package.dependencies] -jupyter-server-fileid = ">=0.6.0,<1" -jupyter-ydoc = ">=0.2.0,<0.4.0" -ypy-websocket = ">=0.8.2,<0.9.0" +anyio = ">=3.1.0" +argon2-cffi = ">=21.1" +jinja2 = ">=3.0.3" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = ">=0.4.4" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = ">=5.0" +packaging = ">=22.0" +prometheus-client = ">=0.9" +pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = ">=1.7" [package.extras] -test = ["coverage", "jupyter-server[test] (>=2.0.0a0)", "pytest (>=7.0)", "pytest-cov", "pytest-timeout", "pytest-tornasync"] +docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] [[package]] -name = "jupyter-ydoc" -version = "0.2.5" -description = "Document structures for collaborative editing using Ypy" -optional = true -python-versions = ">=3.7" +name = "jupyter-server-terminals" +version = "0.5.3" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" files = [ - {file = "jupyter_ydoc-0.2.5-py3-none-any.whl", hash = "sha256:5759170f112c70320a84217dd98d287699076ae65a7f88d458d57940a9f2b882"}, - {file = "jupyter_ydoc-0.2.5.tar.gz", hash = "sha256:5a02ca7449f0d875f73e8cb8efdf695dddef15a8e71378b1f4eda6b7c90f5382"}, + {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, + {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -y-py = ">=0.6.0,<0.7.0" +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" [package.extras] -dev = ["click", "jupyter-releaser"] -test = ["pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)", "ypy-websocket (>=0.8.4,<0.9.0)"] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] [[package]] name = "jupyterlab" -version = "3.6.8" +version = "4.3.0" description = "JupyterLab computational environment" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jupyterlab-3.6.8-py3-none-any.whl", hash = "sha256:891284e75158998e23eb7a23ecc4caaf27b365e41adca374109b1305b9f769db"}, - {file = "jupyterlab-3.6.8.tar.gz", hash = "sha256:a2477383e23f20009188bd9dac7e6e38dbc54307bc36d716bea6ced450647c97"}, + {file = "jupyterlab-4.3.0-py3-none-any.whl", hash = "sha256:f67e1095ad61ae04349024f0b40345062ab108a0c6998d9810fec6a3c1a70cd5"}, + {file = "jupyterlab-4.3.0.tar.gz", hash = "sha256:7c6835cbf8df0af0ec8a39332e85ff11693fb9a468205343b4fc0bfbc74817e5"}, ] [package.dependencies] -ipython = "*" -jinja2 = ">=2.1" +async-lru = ">=1.0.0" +httpx = ">=0.25.0" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +ipykernel = ">=6.5.0" +jinja2 = ">=3.0.3" jupyter-core = "*" -jupyter-server = ">=1.16.0,<3" -jupyter-server-ydoc = ">=0.8.0,<0.9.0" -jupyter-ydoc = ">=0.2.4,<0.3.0" -jupyterlab-server = ">=2.19,<3.0" -nbclassic = "*" -notebook = "<7" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.27.1,<3" +notebook-shim = ">=0.2" packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} -tornado = ">=6.1.0" +setuptools = ">=40.1.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} +tornado = ">=6.2.0" +traitlets = "*" [package.extras] -docs = ["jsx-lexer", "myst-parser", "pytest", "pytest-check-links", "pytest-tornasync", "sphinx (>=1.8)", "sphinx-copybutton", "sphinx-rtd-theme"] -test = ["check-manifest", "coverage", "jupyterlab-server[test]", "pre-commit", "pytest (>=6.0)", "pytest-check-links (>=0.5)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "requests", "requests-cache", "virtualenv"] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-h5web" -version = "8.1.0" +version = "12.3.0" description = "A JupyterLab extension to explore and visualize HDF5 file contents." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jupyterlab_h5web-8.1.0-py3-none-any.whl", hash = "sha256:99c6136500f317f2fbecce97be4b3515d6cf25410fc9274e3243169385a85dfd"}, - {file = "jupyterlab_h5web-8.1.0.tar.gz", hash = "sha256:784c6cafff088725cbc73fef8d6181bb690a1fc414868f74be9a3f57557b16b6"}, + {file = "jupyterlab_h5web-12.3.0-py3-none-any.whl", hash = "sha256:75aafa5c622cd5882161ea4865acdefdbfa59eecea57a1bf3fa6d600297554af"}, + {file = "jupyterlab_h5web-12.3.0.tar.gz", hash = "sha256:117686d4a5185c03f86d6ba1e5da9c51be170d5bc22f5dc930cb5d48a1d46150"}, ] [package.dependencies] -h5grove = "1.3.0" +h5grove = "2.3.0" h5py = ">=3.5" hdf5plugin = {version = "*", optional = true, markers = "extra == \"full\""} -jupyter-server = ">=1.6,<2" +jupyter-server = ">=1.6,<3" [package.extras] full = ["hdf5plugin"] @@ -1966,13 +1996,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "jupyterlab-widgets" -version = "1.1.11" -description = "A JupyterLab extension." +version = "3.0.13" +description = "Jupyter interactive widgets for JupyterLab" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "jupyterlab_widgets-1.1.11-py3-none-any.whl", hash = "sha256:840e538021d87e020a8e7b786597f088431f4ebd8308655555e126c3950a1b27"}, - {file = "jupyterlab_widgets-1.1.11.tar.gz", hash = "sha256:414cdbcd99db6e8f1174c7e4ed49c6ba368779f4659806fb1d824f3c377218e4"}, + {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, + {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, ] [[package]] @@ -2648,28 +2678,6 @@ files = [ fast = ["fastnumbers (>=2.0.0)"] icu = ["PyICU (>=1.0.0)"] -[[package]] -name = "nbclassic" -version = "1.1.0" -description = "Jupyter Notebook as a Jupyter Server extension." -optional = false -python-versions = ">=3.7" -files = [ - {file = "nbclassic-1.1.0-py3-none-any.whl", hash = "sha256:8c0fd6e36e320a18657ff44ed96c3a400f17a903a3744fc322303a515778f2ba"}, - {file = "nbclassic-1.1.0.tar.gz", hash = "sha256:77b77ba85f9e988f9bad85df345b514e9e64c7f0e822992ab1df4a78ac64fc1e"}, -] - -[package.dependencies] -ipykernel = "*" -ipython-genutils = "*" -nest-asyncio = ">=1.5" -notebook-shim = ">=0.2.3" - -[package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-jupyter", "pytest-playwright", "pytest-tornasync", "requests", "requests-unixsocket", "testpath"] - [[package]] name = "nbclient" version = "0.10.0" @@ -2774,7 +2782,7 @@ traitlets = ">=5" name = "nest-asyncio" version = "1.6.0" description = "Patch asyncio to allow nested event loops" -optional = false +optional = true python-versions = ">=3.5" files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, @@ -2812,43 +2820,32 @@ files = [ [[package]] name = "notebook" -version = "6.5.4" -description = "A web-based notebook environment for interactive computing" -optional = false -python-versions = ">=3.7" +version = "7.0.7" +description = "Jupyter Notebook - A web-based notebook environment for interactive computing" +optional = true +python-versions = ">=3.8" files = [ - {file = "notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f"}, - {file = "notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e"}, + {file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"}, + {file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"}, ] [package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=5.3.4" -jupyter-core = ">=4.6.1" -nbclassic = ">=0.4.7" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" +jupyter-server = ">=2.4.0,<3" +jupyterlab = ">=4.0.2,<5" +jupyterlab-server = ">=2.22.1,<3" +notebook-shim = ">=0.2,<0.3" +tornado = ">=6.2.0" [package.extras] -docs = ["myst-parser", "nbsphinx", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -json-logging = ["json-logging"] -test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixsocket", "selenium (==4.1.5)", "testpath"] +dev = ["hatch", "pre-commit"] +docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" version = "0.2.4" description = "A shim layer for notebook traits and config" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, @@ -2958,12 +2955,14 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, ] [[package]] @@ -3033,6 +3032,17 @@ files = [ {file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"}, ] +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + [[package]] name = "packaging" version = "24.2" @@ -3097,9 +3107,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3720,13 +3730,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pynxtools" -version = "0.7.4" +version = "0.8.1" description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." optional = false python-versions = ">=3.8" files = [ - {file = "pynxtools-0.7.4-py3-none-any.whl", hash = "sha256:16a6a52ea8feb5847696bc6e474e31099840ae2fbcd1297bc5c37e756b7e6a62"}, - {file = "pynxtools-0.7.4.tar.gz", hash = "sha256:3e75a60cd92f0fea6eb9d456c135d62cf0f30cbdc54d73e71023d1db42920094"}, + {file = "pynxtools-0.8.1-py3-none-any.whl", hash = "sha256:4d969c10633116515f4532721ad23fcd729d4c2e4208a276977f6ab5c7cb857b"}, + {file = "pynxtools-0.8.1.tar.gz", hash = "sha256:921375e9c8bf47e122a2cae76128e1acab8313217f0b724611214aa841a8e654"}, ] [package.dependencies] @@ -3746,7 +3756,7 @@ xarray = ">=0.20.2" [package.extras] apm = ["pynxtools-apm"] convert = ["pynxtools[apm,ellips,em,mpes,raman,stm,xps,xrd]"] -dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (==0.5.5)", "structlog", "types-pytz", "types-pyyaml", "types-requests", "uv"] +dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (>=0.6)", "structlog", "types-pytz", "types-pyyaml", "types-requests", "uv"] docs = ["markdown-include", "mkdocs", "mkdocs-click", "mkdocs-macros-plugin", "mkdocs-material", "mkdocs-material-extensions"] ellips = ["pynxtools-ellips"] em = ["pynxtools-em"] @@ -3882,7 +3892,7 @@ six = ">=1.5" name = "python-json-logger" version = "2.0.7" description = "A python library adding a json log formatter" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, @@ -4198,7 +4208,7 @@ fixture = ["fixtures"] name = "rfc3339-validator" version = "0.1.4" description = "A pure python RFC3339 validator" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, @@ -4212,7 +4222,7 @@ six = "*" name = "rfc3986-validator" version = "0.1.1" description = "Pure python rfc3986 validator" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, @@ -4469,6 +4479,26 @@ nativelib = ["pyobjc-framework-Cocoa", "pywin32"] objc = ["pyobjc-framework-Cocoa"] win32 = ["pywin32"] +[[package]] +name = "setuptools" +version = "75.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = true +python-versions = ">=3.9" +files = [ + {file = "setuptools-75.4.0-py3-none-any.whl", hash = "sha256:b3c5d862f98500b06ffdf7cc4499b48c46c317d8d56cb30b5c8bce4d88f5c216"}, + {file = "setuptools-75.4.0.tar.gz", hash = "sha256:1dc484f5cf56fd3fe7216d7b8df820802e7246cfb534a1db2aa64f14fcb9cdcb"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] + [[package]] name = "six" version = "1.16.0" @@ -4902,7 +4932,7 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, name = "types-python-dateutil" version = "2.9.0.20241003" description = "Typing stubs for python-dateutil" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, @@ -4977,7 +5007,7 @@ test = ["pytest", "pytest-cov"] name = "uri-template" version = "1.3.0" description = "RFC 6570 URI Template Processor" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, @@ -5039,7 +5069,7 @@ files = [ name = "webcolors" version = "24.11.1" description = "A library for working with the color formats defined by HTML and CSS." -optional = true +optional = false python-versions = ">=3.9" files = [ {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, @@ -5075,18 +5105,15 @@ test = ["websockets"] [[package]] name = "widgetsnbextension" -version = "3.6.10" -description = "IPython HTML widgets for Jupyter" +version = "4.0.13" +description = "Jupyter interactive widgets for Jupyter Notebook" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "widgetsnbextension-3.6.10-py2.py3-none-any.whl", hash = "sha256:91a283c2bb50b43ae415dfe69fb026ece0c14e0102987fb53127c7a71e82417d"}, - {file = "widgetsnbextension-3.6.10.tar.gz", hash = "sha256:cc370876baee1d23d4c506c798ab7d08c355133c9a5e81474159ff75877593df"}, + {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, + {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, ] -[package.dependencies] -notebook = ">=4.4.1" - [[package]] name = "xarray" version = "2024.7.0" @@ -5122,108 +5149,6 @@ files = [ {file = "xyzservices-2024.9.0.tar.gz", hash = "sha256:68fb8353c9dbba4f1ff6c0f2e5e4e596bb9e1db7f94f4f7dfbcb26e25aa66fde"}, ] -[[package]] -name = "y-py" -version = "0.6.2" -description = "Python bindings for the Y-CRDT built from yrs (Rust)" -optional = true -python-versions = "*" -files = [ - {file = "y_py-0.6.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c26bada6cd109095139237a46f50fc4308f861f0d304bc9e70acbc6c4503d158"}, - {file = "y_py-0.6.2-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:bae1b1ad8d2b8cf938a60313f8f7461de609621c5dcae491b6e54975f76f83c5"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e794e44fa260300b8850246c6371d94014753c73528f97f6ccb42f5e7ce698ae"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2686d7d8ca31531458a48e08b0344a8eec6c402405446ce7d838e2a7e43355a"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d917f5bc27b85611ceee4eb85f0e4088b0a03b4eed22c472409933a94ee953cf"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f6071328aad06fdcc0a4acc2dc4839396d645f5916de07584af807eb7c08407"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:266ec46ab9f9cb40fbb5e649f55c329fc4620fa0b1a8117bdeefe91595e182dc"}, - {file = "y_py-0.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce15a842c2a0bf46180ae136743b561fa276300dd7fa61fe76daf00ec7dc0c2d"}, - {file = "y_py-0.6.2-cp310-none-win32.whl", hash = "sha256:1d5b544e79ace93fdbd0b36ed329c86e346898153ac7ba2ec62bc9b4c6b745c9"}, - {file = "y_py-0.6.2-cp310-none-win_amd64.whl", hash = "sha256:80a827e173372682959a57e6b8cc4f6468b1a4495b4bc7a775ef6ca05ae3e8e8"}, - {file = "y_py-0.6.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:a21148b8ea09a631b752d975f9410ee2a31c0e16796fdc113422a6d244be10e5"}, - {file = "y_py-0.6.2-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:898fede446ca1926b8406bdd711617c2aebba8227ee8ec1f0c2f8568047116f7"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce7c20b9395696d3b5425dccf2706d374e61ccf8f3656bff9423093a6df488f5"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3932f53418b408fa03bd002e6dc573a74075c2c092926dde80657c39aa2e054"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df35ea436592eb7e30e59c5403ec08ec3a5e7759e270cf226df73c47b3e739f5"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26cb1307c3ca9e21a3e307ab2c2099677e071ae9c26ec10ddffb3faceddd76b3"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:863e175ce5585f9ff3eba2aa16626928387e2a576157f02c8eb247a218ecdeae"}, - {file = "y_py-0.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35fcb9def6ce137540fdc0e91b08729677548b9c393c0151a6359fd199da3bd7"}, - {file = "y_py-0.6.2-cp311-none-win32.whl", hash = "sha256:86422c6090f34906c062fd3e4fdfdccf3934f2922021e979573ae315050b4288"}, - {file = "y_py-0.6.2-cp311-none-win_amd64.whl", hash = "sha256:6c2f2831c5733b404d2f2da4bfd02bb4612ae18d0822e14ae79b0b92436b816d"}, - {file = "y_py-0.6.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:7cbefd4f1060f05768227ddf83be126397b1d430b026c64e0eb25d3cf50c5734"}, - {file = "y_py-0.6.2-cp312-cp312-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:032365dfe932bfab8e80937ad6093b4c22e67d63ad880096b5fa8768f8d829ba"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a70aee572da3994238c974694767365f237fc5949a550bee78a650fe16f83184"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae80d505aee7b3172cdcc2620ca6e2f85586337371138bb2b71aa377d2c31e9a"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a497ebe617bec6a420fc47378856caae40ab0652e756f3ed40c5f1fe2a12220"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8638355ae2f996356f7f281e03a3e3ce31f1259510f9d551465356532e0302c"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8448da4092265142662bbd3fc46cb8b0796b1e259189c020bc8f738899abd0b5"}, - {file = "y_py-0.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69cfbcbe0a05f43e780e6a198080ba28034bf2bb4804d7d28f71a0379bfd1b19"}, - {file = "y_py-0.6.2-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:1f798165158b76365a463a4f8aa2e3c2a12eb89b1fc092e7020e93713f2ad4dc"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92878cc05e844c8da937204bc34c2e6caf66709ce5936802fbfb35f04132892"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b8822a5c0fd9a8cffcabfcc0cd7326bad537ee614fc3654e413a03137b6da1a"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e13cba03c7af8c8a846c4495875a09d64362cc4caeed495ada5390644411bbe7"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82f2e5b31678065e7a7fa089ed974af5a4f076673cf4f414219bdadfc3246a21"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1935d12e503780b859d343161a80df65205d23cad7b4f6c3df6e50321e188a3"}, - {file = "y_py-0.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd302c6d46a3be57664571a5f0d4224646804be9890a01d73a0b294f2d3bbff1"}, - {file = "y_py-0.6.2-cp37-none-win32.whl", hash = "sha256:5415083f7f10eac25e1c434c87f07cb9bfa58909a6cad6649166fdad21119fc5"}, - {file = "y_py-0.6.2-cp37-none-win_amd64.whl", hash = "sha256:376c5cc0c177f03267340f36aec23e5eaf19520d41428d87605ca2ca3235d845"}, - {file = "y_py-0.6.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:3c011303eb2b360695d2bd4bd7ca85f42373ae89fcea48e7fa5b8dc6fc254a98"}, - {file = "y_py-0.6.2-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c08311db17647a47d4898fc6f8d9c1f0e58b927752c894877ff0c38b3db0d6e1"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7cafbe946b4cafc1e5709957e6dd5c6259d241d48ed75713ded42a5e8a4663"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ba99d0bdbd9cabd65f914cd07b4fb2e939ce199b54ae5ace1639ce1edf8e0a2"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dab84c52f64e10adc79011a08673eb80286c159b14e8fb455524bf2994f0cb38"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72875641a907523d37f4619eb4b303611d17e0a76f2ffc423b62dd1ca67eef41"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c31240e30d5636ded02a54b7280aa129344fe8e964fd63885e85d9a8a83db206"}, - {file = "y_py-0.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c28d977f516d4928f6bc0cd44561f6d0fdd661d76bac7cdc4b73e3c209441d9"}, - {file = "y_py-0.6.2-cp38-none-win32.whl", hash = "sha256:c011997f62d0c3b40a617e61b7faaaf6078e4eeff2e95ce4c45838db537816eb"}, - {file = "y_py-0.6.2-cp38-none-win_amd64.whl", hash = "sha256:ce0ae49879d10610cf3c40f4f376bb3cc425b18d939966ac63a2a9c73eb6f32a"}, - {file = "y_py-0.6.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:47fcc19158150dc4a6ae9a970c5bc12f40b0298a2b7d0c573a510a7b6bead3f3"}, - {file = "y_py-0.6.2-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2d2b054a1a5f4004967532a4b82c6d1a45421ef2a5b41d35b6a8d41c7142aabe"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0787e85645bb4986c27e271715bc5ce21bba428a17964e5ec527368ed64669bc"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:17bce637a89f6e75f0013be68becac3e38dc082e7aefaf38935e89215f0aa64a"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beea5ad9bd9e56aa77a6583b6f4e347d66f1fe7b1a2cb196fff53b7634f9dc84"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1dca48687f41efd862355e58b0aa31150586219324901dbea2989a506e291d4"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17edd21eef863d230ea00004ebc6d582cc91d325e7132deb93f0a90eb368c855"}, - {file = "y_py-0.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de9cfafe97c75cd3ea052a24cd4aabf9fb0cfc3c0f9f810f00121cdf123db9e4"}, - {file = "y_py-0.6.2-cp39-none-win32.whl", hash = "sha256:82f5ca62bedbf35aaf5a75d1f53b4457a1d9b6ff033497ca346e2a0cedf13d14"}, - {file = "y_py-0.6.2-cp39-none-win_amd64.whl", hash = "sha256:7227f232f2daf130ba786f6834548f2cfcfa45b7ec4f0d449e72560ac298186c"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0649a41cd3c98e290c16592c082dbe42c7ffec747b596172eebcafb7fd8767b0"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:bf6020560584671e76375b7a0539e0d5388fc70fa183c99dc769895f7ef90233"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf817a72ffec4295def5c5be615dd8f1e954cdf449d72ebac579ff427951328"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c7302619fc962e53093ba4a94559281491c045c925e5c4defec5dac358e0568"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cd6213c3cf2b9eee6f2c9867f198c39124c557f4b3b77d04a73f30fd1277a59"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b4fac4ea2ce27b86d173ae45765ced7f159120687d4410bb6d0846cbdb170a3"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:932abb560fe739416b50716a72ba6c6c20b219edded4389d1fc93266f3505d4b"}, - {file = "y_py-0.6.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e42258f66ad9f16d9b62e9c9642742982acb1f30b90f5061522048c1cb99814f"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cfc8381df1f0f873da8969729974f90111cfb61a725ef0a2e0e6215408fe1217"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:613f83713714972886e81d71685403098a83ffdacf616f12344b52bc73705107"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316e5e1c40259d482883d1926fd33fa558dc87b2bd2ca53ce237a6fe8a34e473"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:015f7f6c1ce8a83d57955d1dc7ddd57cb633ae00576741a4fc9a0f72ed70007d"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff32548e45e45bf3280ac1d28b3148337a5c6714c28db23aeb0693e33eba257e"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f2d881f0f8bf5674f8fe4774a438c545501e40fa27320c73be4f22463af4b05"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3bbe2f925cc587545c8d01587b4523177408edd252a32ce6d61b97113fe234d"}, - {file = "y_py-0.6.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f5c14d25611b263b876e9ada1701415a13c3e9f02ea397224fbe4ca9703992b"}, - {file = "y_py-0.6.2.tar.gz", hash = "sha256:4757a82a50406a0b3a333aa0122019a331bd6f16e49fed67dca423f928b3fd4d"}, -] - -[[package]] -name = "ypy-websocket" -version = "0.8.4" -description = "WebSocket connector for Ypy" -optional = true -python-versions = ">=3.7" -files = [ - {file = "ypy_websocket-0.8.4-py3-none-any.whl", hash = "sha256:b1ba0dfcc9762f0ca168d2378062d3ca1299d39076b0f145d961359121042be5"}, - {file = "ypy_websocket-0.8.4.tar.gz", hash = "sha256:43a001473f5c8abcf182f603049cf305cbc855ad8deaa9dfa0f3b5a7cea9d0ff"}, -] - -[package.dependencies] -aiofiles = ">=22.1.0,<23" -aiosqlite = ">=0.17.0,<1" -y-py = ">=0.6.0,<0.7.0" - -[package.extras] -test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"] - [[package]] name = "zipp" version = "3.21.0" @@ -5250,4 +5175,4 @@ notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] [metadata] lock-version = "2.0" python-versions = ">=3.9, <3.13" -content-hash = "1f3253586eda248efed89d3a41d876407dfc9024117879799ca6aa7567a14a0e" +content-hash = "896002abae0246496264bdc4727839b3d9c0fb64cbe4b2f1ad8c45e5542e2461" diff --git a/pyproject.toml b/pyproject.toml index f579cce9..6e67d897 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ license = "MIT" python = ">=3.9, <3.13" bokeh = ">=2.4.2" dask = ">=2021.12.0, <2024.8" -docutils = "<0.21" fastdtw = ">=0.3.4" h5py = ">=3.6.0" ipympl = ">=0.9.1" -ipywidgets = "^7.7.1" +ipywidgets = "^8.1.5" lmfit = ">=1.0.3" matplotlib = ">=3.5.1, <3.9.1" natsort = ">=8.1.0" @@ -28,7 +27,7 @@ numpy = ">=1.18, <2.0" pandas = ">=1.4.1" psutil = ">=5.9.0" pynxtools-mpes = "^0.2.0" -pynxtools = "^0.7.0" +pynxtools = "^0.8.0" pyyaml = ">=6.0.0" scipy = ">=1.8.0" symmetrize = ">=0.5.5" @@ -41,8 +40,8 @@ pyarrow = ">=14.0.1, <17.0" pydantic = ">=2.8.2" jupyter = {version = ">=1.0.0", optional = true} ipykernel = {version = ">=6.9.1", optional = true} -jupyterlab = {version = "^3.4.0", optional = true} -jupyterlab-h5web = {version = "^8.0.0", extras = ["full"]} +jupyterlab = {version = ">=4.1.8", optional = true} +jupyterlab-h5web = {version = ">=8.0.0", extras = ["full"]} [tool.poetry.extras] notebook = ["jupyter", "ipykernel", "jupyterlab", "jupyterlab-h5web"] From 1cbfd36a63c79a44f90fd527e0146c50a8a58b02 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 21:26:01 +0200 Subject: [PATCH 245/300] adopt figure sizes --- src/sed/calibrator/energy.py | 19 ++++++++++++------- src/sed/calibrator/momentum.py | 5 ++--- src/sed/core/processor.py | 4 ++-- src/sed/diagnostics.py | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/sed/calibrator/energy.py b/src/sed/calibrator/energy.py index 65493f66..7f1426f9 100644 --- a/src/sed/calibrator/energy.py +++ b/src/sed/calibrator/energy.py @@ -351,7 +351,7 @@ def adjust_ranges( # make plot labels = kwds.pop("labels", [str(b) + " V" for b in self.biases]) - figsize = kwds.pop("figsize", (8, 4)) + figsize = kwds.pop("figsize", (6, 4)) plot_segs = [] plot_peaks = [] fig, ax = plt.subplots(figsize=figsize) @@ -664,15 +664,19 @@ def view( sign = 1 if energy_scale == "kinetic" else -1 + figsize = kwds.pop("figsize", (6, 4)) + if backend == "matplotlib": - figsize = kwds.pop("figsize", (6, 4)) + colors = plt.get_cmap("rainbow")(np.linspace(0, 1, len(traces))) fig_plt, ax = plt.subplots(figsize=figsize) - for itr, trace in enumerate(traces): + for itr, color in zip(range(len(traces)), colors): + trace = traces[itr, :] if align: ax.plot( xaxis + sign * (self.biases[itr]), trace, ls="-", + color=color, linewidth=1, label=lbs[itr], **linekwds, @@ -682,6 +686,7 @@ def view( xaxis, trace, ls="-", + color=color, linewidth=1, label=lbs[itr], **linekwds, @@ -696,6 +701,7 @@ def view( tofseg, traceseg, ls="-", + color=color, linewidth=2, **linesegkwds, ) @@ -710,7 +716,7 @@ def view( if show_legend: try: - ax.legend(fontsize=12, **legkwds) + ax.legend(fontsize=8, loc="upper right", **legkwds) except TypeError: pass @@ -721,11 +727,10 @@ def view( colors = it.cycle(ColorCycle[10]) ttp = [("(x, y)", "($x, $y)")] - figsize = kwds.pop("figsize", (800, 300)) fig = pbk.figure( title=ttl, - width=figsize[0], - height=figsize[1], + width=figsize[0] * 100, + height=figsize[1] * 100, tooltips=ttp, ) # Plotting the main traces diff --git a/src/sed/calibrator/momentum.py b/src/sed/calibrator/momentum.py index 33047292..36a41b86 100644 --- a/src/sed/calibrator/momentum.py +++ b/src/sed/calibrator/momentum.py @@ -1393,11 +1393,10 @@ def view( output_notebook(hide_banner=True) colors = it.cycle(ColorCycle[10]) ttp = [("(x, y)", "($x, $y)")] - figsize = kwds.pop("figsize", (320, 300)) palette = cm2palette(cmap) # Retrieve palette colors fig = pbk.figure( - width=figsize[0], - height=figsize[1], + width=figsize[0] * 100, + height=figsize[1] * 100, tooltips=ttp, x_range=(0, num_rows), y_range=(0, num_cols), diff --git a/src/sed/core/processor.py b/src/sed/core/processor.py index f40089bb..b23dfa74 100644 --- a/src/sed/core/processor.py +++ b/src/sed/core/processor.py @@ -1243,7 +1243,7 @@ def load_bias_series( self.ec.view( traces=self.ec.traces_normed, xaxis=self.ec.tof, - backend="bokeh", + backend="matplotlib", ) # 2. extract ranges and get peak positions @@ -1303,7 +1303,7 @@ def find_bias_peaks( segs=self.ec.featranges, xaxis=self.ec.tof, peaks=self.ec.peaks, - backend="bokeh", + backend="matplotlib", ) except IndexError: logger.error("Could not determine all peaks!") diff --git a/src/sed/diagnostics.py b/src/sed/diagnostics.py index 02bcc0a0..25fddd9a 100644 --- a/src/sed/diagnostics.py +++ b/src/sed/diagnostics.py @@ -88,7 +88,7 @@ def grid_histogram( if legkwds is None: legkwds = {} - figsz = kwds.pop("figsize", (14, 8)) + figsz = kwds.pop("figsize", (10, 7)) if len(kwds) > 0: raise TypeError(f"grid_histogram() got unexpected keyword arguments {kwds.keys()}.") From dd913139515a695a1e461290a6236613e8946d57 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 21:44:43 +0200 Subject: [PATCH 246/300] change color map to default plt cycle --- src/sed/calibrator/energy.py | 9 ++++++--- src/sed/core/processor.py | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/sed/calibrator/energy.py b/src/sed/calibrator/energy.py index 7f1426f9..b5d055dd 100644 --- a/src/sed/calibrator/energy.py +++ b/src/sed/calibrator/energy.py @@ -355,7 +355,7 @@ def adjust_ranges( plot_segs = [] plot_peaks = [] fig, ax = plt.subplots(figsize=figsize) - colors = plt.get_cmap("rainbow")(np.linspace(0, 1, len(traces))) + colors = it.cycle(plt.rcParams["axes.prop_cycle"].by_key()["color"]) for itr, color in zip(range(len(traces)), colors): trace = traces[itr, :] # main traces @@ -378,6 +378,9 @@ def adjust_ranges( plot_peaks.append(scatt) ax.legend(fontsize=8, loc="upper right") ax.set_title("") + plt.xlabel("Time-of-flight") + plt.ylabel("Intensity") + plt.tight_layout() def update(refid, ranges): self.add_ranges(ranges, refid, traces=traces, **kwds) @@ -667,8 +670,8 @@ def view( figsize = kwds.pop("figsize", (6, 4)) if backend == "matplotlib": - colors = plt.get_cmap("rainbow")(np.linspace(0, 1, len(traces))) - fig_plt, ax = plt.subplots(figsize=figsize) + colors = it.cycle(plt.rcParams["axes.prop_cycle"].by_key()["color"]) + _, ax = plt.subplots(figsize=figsize) for itr, color in zip(range(len(traces)), colors): trace = traces[itr, :] if align: diff --git a/src/sed/core/processor.py b/src/sed/core/processor.py index b23dfa74..6c51acfb 100644 --- a/src/sed/core/processor.py +++ b/src/sed/core/processor.py @@ -1245,6 +1245,9 @@ def load_bias_series( xaxis=self.ec.tof, backend="matplotlib", ) + plt.xlabel("Time-of-flight") + plt.ylabel("Intensity") + plt.tight_layout() # 2. extract ranges and get peak positions @call_logger(logger) From 6f5f0333a435e79bc7b1ee814c1a28a7bdf59857 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 22:54:43 +0200 Subject: [PATCH 247/300] use matplotlib also for momentum correction --- .cspell/custom-dictionary.txt | 2 ++ src/sed/calibrator/momentum.py | 22 ++++++++++++++-------- src/sed/core/processor.py | 20 ++++++++++++-------- src/sed/diagnostics.py | 23 +++++++++++++---------- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 38f8c89c..88ff0b12 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -404,6 +404,7 @@ xpos xratio xrng xscale +xticks xtrans Xuser xval @@ -414,6 +415,7 @@ ylabel ypos yratio yscale +yticks ytrans zain Zenodo diff --git a/src/sed/calibrator/momentum.py b/src/sed/calibrator/momentum.py index 36a41b86..534ad81f 100644 --- a/src/sed/calibrator/momentum.py +++ b/src/sed/calibrator/momentum.py @@ -1348,7 +1348,9 @@ def view( if annotated: tsr, tsc = kwds.pop("textshift", (3, 3)) - txtsize = kwds.pop("textsize", 12) + txtsize = kwds.pop("textsize", 10) + + title = kwds.pop("title", "") # Handle unexpected kwds: handled_kwds = {"figsize"} @@ -1358,7 +1360,7 @@ def view( ) if backend == "matplotlib": - fig_plt, ax = plt.subplots(figsize=figsize) + _, ax = plt.subplots(figsize=figsize) ax.imshow(image.T, origin=origin, cmap=cmap, **imkwds) if cross: @@ -1368,15 +1370,12 @@ def view( # Add annotation to the figure if annotated: - for ( - p_keys, # pylint: disable=unused-variable - p_vals, - ) in points.items(): + for p_keys, p_vals in points.items(): try: - ax.scatter(p_vals[:, 0], p_vals[:, 1], **scatterkwds) + ax.scatter(p_vals[:, 0], p_vals[:, 1], s=15, **scatterkwds) except IndexError: try: - ax.scatter(p_vals[0], p_vals[1], **scatterkwds) + ax.scatter(p_vals[0], p_vals[1], s=15, **scatterkwds) except IndexError: pass @@ -1389,6 +1388,13 @@ def view( fontsize=txtsize, ) + if crosshair and self.pcent is not None: + for radius in crosshair_radii: + circle = plt.Circle(self.pcent, radius, color="k", fill=False) + ax.add_patch(circle) + + ax.set_title(title) + elif backend == "bokeh": output_notebook(hide_banner=True) colors = it.cycle(ColorCycle[10]) diff --git a/src/sed/core/processor.py b/src/sed/core/processor.py index 6c51acfb..5888855d 100644 --- a/src/sed/core/processor.py +++ b/src/sed/core/processor.py @@ -649,24 +649,28 @@ def generate_splinewarp( self.mc.spline_warp_estimate(use_center=use_center, **kwds) if self.mc.slice is not None and self._verbose: - print("Original slice with reference features") - self.mc.view(annotated=True, backend="bokeh", crosshair=True) + self.mc.view( + annotated=True, + backend="matplotlib", + crosshair=True, + title="Original slice with reference features", + ) - print("Corrected slice with target features") self.mc.view( image=self.mc.slice_corrected, annotated=True, points={"feats": self.mc.ptargs}, - backend="bokeh", + backend="matplotlib", crosshair=True, + title="Corrected slice with target features", ) - print("Original slice with target features") self.mc.view( image=self.mc.slice, points={"feats": self.mc.ptargs}, annotated=True, - backend="bokeh", + backend="matplotlib", + title="Original slice with target features", ) # 3a. Save spline-warp parameters to config file. @@ -2384,7 +2388,7 @@ def view_event_histogram( bins: Sequence[int] = None, axes: Sequence[str] = None, ranges: Sequence[tuple[float, float]] = None, - backend: str = "bokeh", + backend: str = "matplotlib", legend: bool = True, histkwds: dict = None, legkwds: dict = None, @@ -2403,7 +2407,7 @@ def view_event_histogram( ranges (Sequence[tuple[float, float]], optional): Value ranges of all specified axes. Defaults to config["histogram"]["ranges"]. backend (str, optional): Backend of the plotting library - ('matplotlib' or 'bokeh'). Defaults to "bokeh". + ("matplotlib" or "bokeh"). Defaults to "matplotlib". legend (bool, optional): Option to include a legend in the histogram plots. Defaults to True. histkwds (dict, optional): Keyword arguments for histograms diff --git a/src/sed/diagnostics.py b/src/sed/diagnostics.py index 25fddd9a..6ec8e507 100644 --- a/src/sed/diagnostics.py +++ b/src/sed/diagnostics.py @@ -59,7 +59,7 @@ def grid_histogram( rvs: Sequence, rvbins: Sequence, rvranges: Sequence[tuple[float, float]], - backend: str = "bokeh", + backend: str = "matplotlib", legend: bool = True, histkwds: dict = None, legkwds: dict = None, @@ -73,22 +73,22 @@ def grid_histogram( rvs (Sequence): List of names for the random variables (rvs). rvbins (Sequence): Bin values for all random variables. rvranges (Sequence[tuple[float, float]]): Value ranges of all random variables. - backend (str, optional): Backend for making the plot ('matplotlib' or 'bokeh'). - Defaults to "bokeh". + backend (str, optional): Backend for making the plot ("matplotlib" or "bokeh"). + Defaults to "matplotlib". legend (bool, optional): Option to include a legend in each histogram plot. Defaults to True. histkwds (dict, optional): Keyword arguments for histogram plots. Defaults to None. legkwds (dict, optional): Keyword arguments for legends. Defaults to None. **kwds: - - *figsize*: Figure size. Defaults to (14, 8) + - *figsize*: Figure size. Defaults to (6, 4) """ if histkwds is None: histkwds = {} if legkwds is None: legkwds = {} - figsz = kwds.pop("figsize", (10, 7)) + figsz = kwds.pop("figsize", (6, 4)) if len(kwds) > 0: raise TypeError(f"grid_histogram() got unexpected keyword arguments {kwds.keys()}.") @@ -96,7 +96,7 @@ def grid_histogram( if backend == "matplotlib": nrv = len(rvs) nrow = int(np.ceil(nrv / ncol)) - histtype = kwds.pop("histtype", "step") + histtype = kwds.pop("histtype", "bar") fig, ax = plt.subplots(nrow, ncol, figsize=figsz) otherax = ax.copy() @@ -114,7 +114,7 @@ def grid_histogram( **histkwds, ) if legend: - ax[axind].legend(fontsize=15, **legkwds) + ax[axind].legend(fontsize=10, **legkwds) otherax[axind] = None @@ -128,13 +128,16 @@ def grid_histogram( **histkwds, ) if legend: - ax[i].legend(fontsize=15, **legkwds) + ax[i].legend(fontsize=10, **legkwds) otherax[i] = None for oax in otherax.flatten(): if oax is not None: fig.delaxes(oax) + plt.xticks(fontsize=8) + plt.yticks(fontsize=8) + plt.tight_layout() elif backend == "bokeh": output_notebook(hide_banner=True) @@ -163,7 +166,7 @@ def grid_histogram( gridplot( plots, # type: ignore ncols=ncol, - width=figsz[0] * 30, - height=figsz[1] * 28, + width=figsz[0] * 100 // ncol, + height=figsz[1] * 100 // (len(plots) // ncol), ), ) From 366de39badeb9bae4413acec8eff344e78f3c7b8 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 15 Oct 2024 22:59:17 +0200 Subject: [PATCH 248/300] fix type error --- src/sed/calibrator/momentum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sed/calibrator/momentum.py b/src/sed/calibrator/momentum.py index 534ad81f..75b7425f 100644 --- a/src/sed/calibrator/momentum.py +++ b/src/sed/calibrator/momentum.py @@ -102,7 +102,7 @@ def __init__( self.include_center: bool = False self.use_center: bool = False self.pouter: np.ndarray = None - self.pcent: tuple[float, ...] = None + self.pcent: tuple[float, float] = None self.pouter_ord: np.ndarray = None self.prefs: np.ndarray = None self.ptargs: np.ndarray = None From 6365b930166c2f083a0c4fd8854ff5ace680936f Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 16 Oct 2024 13:26:24 +0200 Subject: [PATCH 249/300] fix figure sizes and font sizes --- .cspell/custom-dictionary.txt | 3 +++ src/sed/diagnostics.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 88ff0b12..546e29ae 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -334,6 +334,7 @@ scicat SDIAG sdir segs +setp sfile shutil Sixten @@ -404,6 +405,7 @@ xpos xratio xrng xscale +xticklabels xticks xtrans Xuser @@ -415,6 +417,7 @@ ylabel ypos yratio yscale +yticklabels yticks ytrans zain diff --git a/src/sed/diagnostics.py b/src/sed/diagnostics.py index 6ec8e507..4f44b1f7 100644 --- a/src/sed/diagnostics.py +++ b/src/sed/diagnostics.py @@ -88,7 +88,7 @@ def grid_histogram( if legkwds is None: legkwds = {} - figsz = kwds.pop("figsize", (6, 4)) + figsz = kwds.pop("figsize", (3, 2)) # figure size of each panel if len(kwds) > 0: raise TypeError(f"grid_histogram() got unexpected keyword arguments {kwds.keys()}.") @@ -98,13 +98,16 @@ def grid_histogram( nrow = int(np.ceil(nrv / ncol)) histtype = kwds.pop("histtype", "bar") - fig, ax = plt.subplots(nrow, ncol, figsize=figsz) + figsize = [figsz[0] * ncol, figsz[1] * nrow] + fig, ax = plt.subplots(nrow, ncol, figsize=figsize) otherax = ax.copy() for i, zipped in enumerate(zip(rvs, rvbins, rvranges)): # Make each histogram plot rvname, rvbin, rvrg = zipped try: axind = np.unravel_index(i, (nrow, ncol)) + plt.setp(ax[axind].get_xticklabels(), fontsize=8) + plt.setp(ax[axind].get_yticklabels(), fontsize=8) ax[axind].hist( dct[rvname], bins=rvbin, @@ -119,6 +122,8 @@ def grid_histogram( otherax[axind] = None except IndexError: + plt.setp(ax[i].get_xticklabels(), fontsize=8) + plt.setp(ax[i].get_yticklabels(), fontsize=8) ax[i].hist( dct[rvname], bins=rvbin, @@ -135,8 +140,7 @@ def grid_histogram( for oax in otherax.flatten(): if oax is not None: fig.delaxes(oax) - plt.xticks(fontsize=8) - plt.yticks(fontsize=8) + plt.tight_layout() elif backend == "bokeh": @@ -166,7 +170,7 @@ def grid_histogram( gridplot( plots, # type: ignore ncols=ncol, - width=figsz[0] * 100 // ncol, - height=figsz[1] * 100 // (len(plots) // ncol), + width=figsz[0] * 100, + height=figsz[1] * 100, ), ) From fb0b025a0d653387d0748dc1b9e0326c27677492 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 13 Nov 2024 09:11:28 +0100 Subject: [PATCH 250/300] remove redundant toctree entry --- docs/user_guide/advanced_topics.md | 6 ------ docs/user_guide/index.md | 5 +++-- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 docs/user_guide/advanced_topics.md diff --git a/docs/user_guide/advanced_topics.md b/docs/user_guide/advanced_topics.md deleted file mode 100644 index ebd773c8..00000000 --- a/docs/user_guide/advanced_topics.md +++ /dev/null @@ -1,6 +0,0 @@ -```{toctree} -:maxdepth: 1 -../tutorial/6_binning_with_time-stamped_data -../tutorial/7_correcting_orthorhombic_symmetry -../tutorial/8_jittering_tutorial -``` diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 16d36aaf..12485b87 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -22,8 +22,9 @@ config ``` ## Advanced Topics - ```{toctree} :maxdepth: 1 -advanced_topics +../tutorial/6_binning_with_time-stamped_data +../tutorial/7_correcting_orthorhombic_symmetry +../tutorial/8_jittering_tutorial ``` From 7ff8fc2a7a1ee4f9873035085f6cdb00b2f50077 Mon Sep 17 00:00:00 2001 From: rettigl Date: Wed, 13 Nov 2024 09:40:04 +0100 Subject: [PATCH 251/300] fix config docs --- docs/user_guide/config.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/config.md b/docs/user_guide/config.md index 7fb62f7d..450d4da2 100644 --- a/docs/user_guide/config.md +++ b/docs/user_guide/config.md @@ -12,18 +12,18 @@ The config mechanism returns the combined dictionary, and reports the loaded con ## Default configuration settings -```{literalinclude} ../../sed/config/default.yaml +```{literalinclude} ../../config/default.yaml :language: yaml ``` ## Example configuration file for mpes (METIS momentum microscope at FHI-Berlin) -```{literalinclude} ../../sed/config/mpes_example_config.yaml +```{literalinclude} ../../config/mpes_example_config.yaml :language: yaml ``` ## Example configuration file for flash (HEXTOF momentum microscope at FLASH, Desy) -```{literalinclude} ../../sed/config/flash_example_config.yaml +```{literalinclude} ../../config/flash_example_config.yaml :language: yaml ``` From f4350d87c2753a15eafb897f1f2bb5ae6f87e489 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 30 Nov 2024 11:00:05 +0100 Subject: [PATCH 252/300] fix config paths --- docs/scripts/build_flash_parquets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/scripts/build_flash_parquets.py b/docs/scripts/build_flash_parquets.py index 9ebb2f30..2c6c2482 100644 --- a/docs/scripts/build_flash_parquets.py +++ b/docs/scripts/build_flash_parquets.py @@ -37,8 +37,8 @@ config_override = { "core": { "paths": { - "data_raw_dir": data_path, - "data_parquet_dir": data_path + "/processed/", + "raw": data_path, + "processed": data_path + "/processed/", }, }, } From 02d8a7187a7c35c341960527fb6b5913bbea3b09 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 30 Nov 2024 21:35:05 +0100 Subject: [PATCH 253/300] fix new notebooks and merging bugs --- docs/sed/config.rst | 2 +- src/sed/core/processor.py | 8 +-- ...hextof_workflow_trXPS_bam_correction.ipynb | 52 ++++++++-------- ...ow_trXPS_energy_calibration_using_SB.ipynb | 59 ++++++++++--------- tutorial/9_hextof_workflow_trXPD.ipynb | 52 ++++++++-------- 5 files changed, 93 insertions(+), 80 deletions(-) diff --git a/docs/sed/config.rst b/docs/sed/config.rst index ca01cfcd..485bd7ea 100644 --- a/docs/sed/config.rst +++ b/docs/sed/config.rst @@ -1,6 +1,6 @@ Config *************************************************** -.. automobile:: sed.core.config +.. automodule:: sed.core.config :members: :undoc-members: diff --git a/src/sed/core/processor.py b/src/sed/core/processor.py index 3862a547..7d876a27 100644 --- a/src/sed/core/processor.py +++ b/src/sed/core/processor.py @@ -1380,10 +1380,10 @@ def calibrate_energy_axis( backend="matplotlib", title="Quality of Calibration", ) - plt.xlabel("Energy (eV)") - plt.ylabel("Intensity") - plt.tight_layout() - plt.show() + plt.xlabel("Energy (eV)") + plt.ylabel("Intensity") + plt.tight_layout() + plt.show() if energy_scale == "kinetic": self.ec.view( traces=self.ec.calibration["axis"][None, :] + self.ec.biases[0], diff --git a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb index 1b7e0d54..6a4c64a5 100644 --- a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb +++ b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb @@ -96,7 +96,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../sed/config/flash_example_config.yaml')\n", + "config_file = Path('../config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, @@ -112,8 +112,8 @@ " \"core\": {\n", " \"beamtime_id\": 11019101,\n", " \"paths\": {\n", - " \"data_raw_dir\": path,\n", - " \"data_parquet_dir\": buffer_path\n", + " \"raw\": path,\n", + " \"processed\": buffer_path\n", " },\n", " },\n", "}" @@ -129,26 +129,24 @@ "energy_cal = {\n", " \"energy\": {\n", " \"calibration\": {\n", - " \"E0\": -53.96145014592986,\n", - " \"creation_date\": 1732056868.029444,\n", - " \"d\": 0.8096677233434938,\n", + " \"E0\": -132.47100427179566,\n", + " \"creation_date\": '2024-11-30T20:47:03.305244',\n", + " \"d\": 0.8096677238144319,\n", " \"energy_scale\": \"kinetic\",\n", - " \"t0\": 4.0148196718030886e-07,\n", + " \"t0\": 4.0148196706891397e-07,\n", " },\n", " \"offsets\":{\n", - " \"constant\": -77.5,\n", - " \"creation_date\": 1732056874.060922,\n", - " \"monochromatorPhotonEnergy\": {\n", - " \"preserve_mean\": True,\n", - " \"weight\": -1,\n", - " },\n", - " \"sampleBias\": {\n", - " \"preserve_mean\": False,\n", - " \"weight\": 1,\n", - " },\n", - " \"tofVoltage\": {\n", - " \"preserve_mean\": True,\n", - " \"weight\": -1,\n", + " \"constant\": 1,\n", + " \"creation_date\": '2024-11-30T21:17:07.762199',\n", + " \"columns\": {\n", + " \"monochromatorPhotonEnergy\": {\n", + " \"preserve_mean\": True,\n", + " \"weight\": -1,\n", + " },\n", + " \"tofVoltage\": {\n", + " \"preserve_mean\": True,\n", + " \"weight\": -1,\n", + " },\n", " },\n", " },\n", " },\n", @@ -230,7 +228,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [1446.75,1449.15]]\n", "bins = [200,40]\n", - "res = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" + "res = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" ] }, { @@ -396,7 +394,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" ] }, { @@ -535,11 +533,19 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2e6e852", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "python3", + "display_name": "sed-processor-7Jy-bAA8-py3.9", "language": "python", "name": "python3" }, diff --git a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb index 3c89547c..b672ce47 100644 --- a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb +++ b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb @@ -97,7 +97,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../sed/config/flash_example_config.yaml')\n", + "config_file = Path('../config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, @@ -113,8 +113,8 @@ " \"core\": {\n", " \"beamtime_id\": 11019101,\n", " \"paths\": {\n", - " \"data_raw_dir\": path,\n", - " \"data_parquet_dir\": buffer_path\n", + " \"raw\": path,\n", + " \"processed\": buffer_path\n", " },\n", " },\n", "}" @@ -175,14 +175,6 @@ "sp_44455.find_bias_peaks(ranges=ranges, ref_id=ref_id, apply=True)" ] }, - { - "cell_type": "markdown", - "id": "d3e4f30d", - "metadata": {}, - "source": [ - "We offset the reference energy by the different in bias voltages between this run and run 44498" - ] - }, { "cell_type": "code", "execution_count": null, @@ -191,14 +183,12 @@ "outputs": [], "source": [ "sp_44455.calibrate_energy_axis(\n", - " ref_id=0,\n", - " ref_energy=-34.9,\n", + " ref_energy=-31.4,\n", " method=\"lmfit\",\n", " energy_scale='kinetic',\n", " d={'value':1.0,'min': .7, 'max':1.0, 'vary':True},\n", " t0={'value':5e-7, 'min': 1e-7, 'max': 1e-6, 'vary':True},\n", " E0={'value': 0., 'min': -200, 'max': 100, 'vary': True},\n", - " verbose=True,\n", ")" ] }, @@ -225,7 +215,8 @@ "id": "98266c62-ab48-4746-96c8-2d47cf92c0e9", "metadata": {}, "source": [ - "### Now we can use those parameters and load our trXPS data using the additional config file" + "### Now we can use those parameters and load our trXPS data using the additional config file\n", + "To obtain a correct energy axis, we offset the energy axis by the difference of photon energy between this run and the energy calibration runs" ] }, { @@ -238,7 +229,13 @@ "run_number = 44498\n", "sp_44498 = SedProcessor(runs=[run_number], config=config_override, folder_config=\"reference_calib.yaml\", system_config=config_file, verbose=True)\n", "sp_44498.add_jitter()\n", - "sp_44498.append_energy_axis()" + "sp_44498.append_energy_axis()\n", + "sp_44498.add_energy_offset(\n", + " constant=1,\n", + " columns=['monochromatorPhotonEnergy','tofVoltage'],\n", + " weights=[-1,-1],\n", + " preserve_mean=[True, True],\n", + ")" ] }, { @@ -328,14 +325,14 @@ "axes = ['dldTimeSteps', 'delayStage']\n", "ranges = [[3900,4200], [-1.5,1.5]]\n", "bins = [100,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")\n", + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)\n", "\n", "fig,ax = plt.subplots(1,2,figsize=(8,3), layout='constrained')\n", "fig.suptitle(f\"Run {run_number}: W 4f, side bands\")\n", - "res_corr.plot(robust=True, ax=ax[0], cmap='terrain')\n", + "res_corr.plot(ax=ax[0], cmap='terrain')\n", "ax[0].set_title('raw')\n", "bg = res_corr.sel(delayStage=slice(-1.3,-1.0)).mean('delayStage')\n", - "(res_corr-bg).plot(robust=True, ax=ax[1])\n", + "(res_corr-bg).plot(ax=ax[1])\n", "ax[1].set_title('difference')" ] }, @@ -395,21 +392,18 @@ "outputs": [], "source": [ "### Kinetic energy of w4f peaks and their SB\n", - "ref_energy = -31.4\n", - "ref_id = 1\n", + "ref_energy = -30.2\n", "sp_44498.ec.biases = -1*np.array([-30.2,-31.4,-32.6,-33.6,-34.8])\n", "sp_44498.ec.peaks = np.expand_dims(data[peaks]['dldTimeSteps'].data,1)\n", "sp_44498.ec.tof = res_corr.dldTimeSteps.data\n", "\n", "sp_44498.calibrate_energy_axis(\n", - " ref_id=ref_id,\n", " ref_energy=ref_energy,\n", " method=\"lmfit\",\n", " d={'value':1.0,'min': .8, 'max':1.0, 'vary':True},\n", " t0={'value':5e-7, 'min': 1e-7, 'max': 1e-6, 'vary':True},\n", " E0={'value': -100., 'min': -200, 'max': 15, 'vary': True},\n", - " labels=\"\",\n", - " verbose=True)" + ")" ] }, { @@ -417,7 +411,8 @@ "id": "4052d629-1178-4248-a945-d60a6ff34bf3", "metadata": {}, "source": [ - "### Append energy axis into a data frame, bin and visualize data in the calibrated energy and corrected delay axis " + "### Append energy axis into a data frame, bin and visualize data in the calibrated energy and corrected delay axis\n", + "To get a correct energy axis, we undo the shifts imposed by the calibration function" ] }, { @@ -427,7 +422,13 @@ "metadata": {}, "outputs": [], "source": [ - "sp_44498.append_energy_axis()" + "sp_44498.append_energy_axis()\n", + "sp_44498.add_energy_offset(\n", + " constant=30.2,\n", + " columns=['monochromatorPhotonEnergy','tofVoltage','sampleBias'],\n", + " weights=[-1,-1,-1],\n", + " preserve_mean=[True, True,False],\n", + ")" ] }, { @@ -440,14 +441,14 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")\n", + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)\n", "\n", "fig,ax = plt.subplots(1,2,figsize=(8,3), layout='constrained')\n", "fig.suptitle(f\"Run {run_number}: W 4f, side bands\")\n", - "res_corr.plot(robust=True, ax=ax[0], cmap='terrain')\n", + "res_corr.plot(ax=ax[0], cmap='terrain')\n", "ax[0].set_title('raw')\n", "bg = res_corr.sel(delayStage=slice(-1.3,-1.0)).mean('delayStage')\n", - "(res_corr-bg).plot(robust=True, ax=ax[1])\n", + "(res_corr-bg).plot(ax=ax[1])\n", "ax[1].set_title('difference')" ] }, diff --git a/tutorial/9_hextof_workflow_trXPD.ipynb b/tutorial/9_hextof_workflow_trXPD.ipynb index 90f3ef63..f2c53f07 100644 --- a/tutorial/9_hextof_workflow_trXPD.ipynb +++ b/tutorial/9_hextof_workflow_trXPD.ipynb @@ -95,7 +95,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../sed/config/flash_example_config.yaml')\n", + "config_file = Path('../config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, @@ -111,8 +111,8 @@ " \"core\": {\n", " \"beamtime_id\": 11019101,\n", " \"paths\": {\n", - " \"data_raw_dir\": path,\n", - " \"data_parquet_dir\": buffer_path\n", + " \"raw\": path,\n", + " \"processed\": buffer_path\n", " },\n", " },\n", "}" @@ -142,25 +142,31 @@ "metadata": {}, "outputs": [], "source": [ - "en_cal_config = {\n", - " 'energy': {\n", - " 'calibration': {\n", - " 'E0': -54.971004271795664,\n", - " 'creation_date': 1718801358.232129,\n", - " 'd': 0.8096677238144319,\n", - " 'energy_scale': 'kinetic',\n", - " 't0': 4.0148196706891397e-07,\n", - " 'calib_type': 'fit',\n", - " 'fit_function': '(a0/(x0-a1))**2 + a2',\n", - " 'coefficients': ([ 8.09667724e-01, 4.01481967e-07, -5.49710043e+01]),\n", - " 'axis': 0},\n", - " 'tof': None,\n", - " 'offsets': {\n", - " 'constant': -76.5,\n", - " 'creation_date': 1718801360.817963,\n", - " 'monochromatorPhotonEnergy': {'preserve_mean': True,'reduction': None,'weight': -1},\n", - " 'sampleBias': {'preserve_mean': False, 'reduction': None, 'weight': 1},\n", - " 'tofVoltage': {'preserve_mean': True, 'reduction': None, 'weight': -1}}}}" + "energy_cal = {\n", + " \"energy\": {\n", + " \"calibration\": {\n", + " \"E0\": -132.47100427179566,\n", + " \"creation_date\": '2024-11-30T20:47:03.305244',\n", + " \"d\": 0.8096677238144319,\n", + " \"energy_scale\": \"kinetic\",\n", + " \"t0\": 4.0148196706891397e-07,\n", + " },\n", + " \"offsets\":{\n", + " \"constant\": 1,\n", + " \"creation_date\": '2024-11-30T21:17:07.762199',\n", + " \"columns\": {\n", + " \"monochromatorPhotonEnergy\": {\n", + " \"preserve_mean\": True,\n", + " \"weight\": -1,\n", + " },\n", + " \"tofVoltage\": {\n", + " \"preserve_mean\": True,\n", + " \"weight\": -1,\n", + " },\n", + " },\n", + " },\n", + " },\n", + "}" ] }, { @@ -275,7 +281,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" ] }, { From 787447d986f3359d3c38d416b907279fb6b81228 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 30 Nov 2024 22:45:01 +0100 Subject: [PATCH 254/300] fix changed variable name --- tutorial/9_hextof_workflow_trXPD.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorial/9_hextof_workflow_trXPD.ipynb b/tutorial/9_hextof_workflow_trXPD.ipynb index f2c53f07..a50cc442 100644 --- a/tutorial/9_hextof_workflow_trXPD.ipynb +++ b/tutorial/9_hextof_workflow_trXPD.ipynb @@ -186,7 +186,7 @@ "outputs": [], "source": [ "run_number = 44498\n", - "sp_44498 = SedProcessor(runs=[run_number], folder_config=en_cal_config, config=config_override, system_config=config_file, verbose=True)\n", + "sp_44498 = SedProcessor(runs=[run_number], folder_config=energy_cal, config=config_override, system_config=config_file, verbose=True)\n", "sp_44498.add_jitter()" ] }, From 637504dfb6664c5bf5ce019d29bf831548d322eb Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 19 Dec 2024 16:54:42 +0100 Subject: [PATCH 255/300] fix merge errors --- src/sed/loader/mpes/loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sed/loader/mpes/loader.py b/src/sed/loader/mpes/loader.py index c9a776c3..92c64b05 100644 --- a/src/sed/loader/mpes/loader.py +++ b/src/sed/loader/mpes/loader.py @@ -828,7 +828,7 @@ def get_start_and_end_time(self) -> tuple[float, float]: Returns: tuple[float, float]: A tuple containing the start and end time stamps """ - h5file = load_h5_in_memory(self.files[0]) + h5filename = self.files[0] channels = [] for channel in self._config["dataframe"]["channels"].values(): if channel["format"] == "per_electron": @@ -837,14 +837,14 @@ def get_start_and_end_time(self) -> tuple[float, float]: if not channels: raise ValueError("No valid 'per_electron' channels found.") timestamps = hdf5_to_array( - h5file, + h5filename=h5filename, channels=channels, time_stamps=True, ) ts_from = timestamps[-1][1] - h5file = load_h5_in_memory(self.files[-1]) + h5filename = self.files[-1] timestamps = hdf5_to_array( - h5file, + h5filename=h5filename, channels=channels, time_stamps=True, ) From 0f395231e8fc1be5b147f45ae4fb3a516122c283 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 21 Dec 2024 23:04:53 +0100 Subject: [PATCH 256/300] move config into sed --- {config => src/sed/config}/NXmpes_config-HEXTOF.json | 0 {config => src/sed/config}/NXmpes_config.json | 0 {config => src/sed/config}/datasets.json | 0 {config => src/sed/config}/default.yaml | 0 {config => src/sed/config}/flash_example_config.yaml | 0 {config => src/sed/config}/mpes_example_config.yaml | 0 {config => src/sed/config}/sxp_example_config.yaml | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {config => src/sed/config}/NXmpes_config-HEXTOF.json (100%) rename {config => src/sed/config}/NXmpes_config.json (100%) rename {config => src/sed/config}/datasets.json (100%) rename {config => src/sed/config}/default.yaml (100%) rename {config => src/sed/config}/flash_example_config.yaml (100%) rename {config => src/sed/config}/mpes_example_config.yaml (100%) rename {config => src/sed/config}/sxp_example_config.yaml (100%) diff --git a/config/NXmpes_config-HEXTOF.json b/src/sed/config/NXmpes_config-HEXTOF.json similarity index 100% rename from config/NXmpes_config-HEXTOF.json rename to src/sed/config/NXmpes_config-HEXTOF.json diff --git a/config/NXmpes_config.json b/src/sed/config/NXmpes_config.json similarity index 100% rename from config/NXmpes_config.json rename to src/sed/config/NXmpes_config.json diff --git a/config/datasets.json b/src/sed/config/datasets.json similarity index 100% rename from config/datasets.json rename to src/sed/config/datasets.json diff --git a/config/default.yaml b/src/sed/config/default.yaml similarity index 100% rename from config/default.yaml rename to src/sed/config/default.yaml diff --git a/config/flash_example_config.yaml b/src/sed/config/flash_example_config.yaml similarity index 100% rename from config/flash_example_config.yaml rename to src/sed/config/flash_example_config.yaml diff --git a/config/mpes_example_config.yaml b/src/sed/config/mpes_example_config.yaml similarity index 100% rename from config/mpes_example_config.yaml rename to src/sed/config/mpes_example_config.yaml diff --git a/config/sxp_example_config.yaml b/src/sed/config/sxp_example_config.yaml similarity index 100% rename from config/sxp_example_config.yaml rename to src/sed/config/sxp_example_config.yaml From d76deabc7bee543d9b4d782334b0d587d004e081 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 22 Dec 2024 00:02:31 +0100 Subject: [PATCH 257/300] fix config references --- .github/workflows/documentation.yml | 3 ++- benchmarks/benchmark_sed.py | 6 +++--- docs/sed/dataset.rst | 2 +- docs/user_guide/config.md | 6 +++--- pyproject.toml | 1 - src/sed/config/flash_example_config.yaml | 2 +- src/sed/config/mpes_example_config.yaml | 2 +- src/sed/core/config.py | 2 +- src/sed/core/dfops.py | 4 ++-- src/sed/dataset/dataset.py | 4 +++- tests/test_dataset.py | 2 +- tests/test_processor.py | 8 ++++---- tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb | 2 +- ...extof_workflow_trXPS_energy_calibration_using_SB.ipynb | 2 +- ...on_pipeline_for_example_time-resolved_ARPES_data.ipynb | 2 +- tutorial/3_metadata_collection_and_export_to_NeXus.ipynb | 2 +- tutorial/4_hextof_workflow.ipynb | 2 +- tutorial/5_sxp_workflow.ipynb | 2 +- tutorial/6_binning_with_time-stamped_data.ipynb | 2 +- tutorial/7_correcting_orthorhombic_symmetry.ipynb | 2 +- tutorial/8_jittering_tutorial.ipynb | 2 +- tutorial/9_hextof_workflow_trXPD.ipynb | 2 +- 22 files changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 6afef727..7027de7e 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -50,7 +50,8 @@ jobs: - name: copy tutorial files to docs run: | cp -r $GITHUB_WORKSPACE/tutorial $GITHUB_WORKSPACE/docs/ - cp -r $GITHUB_WORKSPACE/config $GITHUB_WORKSPACE/docs/ + mkdir -p $GITHUB_WORKSPACE/docs/src/sed + cp -r $GITHUB_WORKSPACE/src/sed/config $GITHUB_WORKSPACE/docs/src/sed/ - name: download RAW data # if: steps.cache-primes.outputs.cache-hit != 'true' diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index af016608..74c8de23 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -90,7 +90,7 @@ def test_splinewarp() -> None: """Run a benchmark for the generation of the inverse dfield correction""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/../../config/mpes_example_config.yaml", + config=package_dir + "/config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -116,7 +116,7 @@ def test_workflow_1d() -> None: """Run a benchmark for 1d binning of converted data""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/../../config/mpes_example_config.yaml", + config=package_dir + "/config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -152,7 +152,7 @@ def test_workflow_4d() -> None: """Run a benchmark for 4d binning of converted data""" processor = SedProcessor( dataframe=dataframe.copy(), - config=package_dir + "/../../config/mpes_example_config.yaml", + config=package_dir + "/config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, diff --git a/docs/sed/dataset.rst b/docs/sed/dataset.rst index b0147464..e282423e 100644 --- a/docs/sed/dataset.rst +++ b/docs/sed/dataset.rst @@ -318,7 +318,7 @@ we can remove one instance Default datasets.json --------------------- -.. literalinclude:: ../../sed/dataset/datasets.json +.. literalinclude:: ../../src/sed/config/datasets.json :language: json API diff --git a/docs/user_guide/config.md b/docs/user_guide/config.md index 450d4da2..bfc6e4da 100644 --- a/docs/user_guide/config.md +++ b/docs/user_guide/config.md @@ -12,18 +12,18 @@ The config mechanism returns the combined dictionary, and reports the loaded con ## Default configuration settings -```{literalinclude} ../../config/default.yaml +```{literalinclude} ../../src/sed/config/default.yaml :language: yaml ``` ## Example configuration file for mpes (METIS momentum microscope at FHI-Berlin) -```{literalinclude} ../../config/mpes_example_config.yaml +```{literalinclude} ../../src/sed/config/mpes_example_config.yaml :language: yaml ``` ## Example configuration file for flash (HEXTOF momentum microscope at FLASH, Desy) -```{literalinclude} ../../config/flash_example_config.yaml +```{literalinclude} ../../src/sed/config/flash_example_config.yaml :language: yaml ``` diff --git a/pyproject.toml b/pyproject.toml index 8633d37f..15a3d662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [tool.poetry] name = "sed-processor" packages = [{include = "sed", from = "src"}] -include = ["config"] version = "1.0.0a0" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = ["OpenCOMPES team "] diff --git a/src/sed/config/flash_example_config.yaml b/src/sed/config/flash_example_config.yaml index c9168a98..232a56bb 100644 --- a/src/sed/config/flash_example_config.yaml +++ b/src/sed/config/flash_example_config.yaml @@ -218,4 +218,4 @@ dataframe: nexus: reader: "mpes" definition: "NXmpes" - input_files: ["../config/NXmpes_config-HEXTOF.json"] + input_files: ["../src/sed/config/NXmpes_config-HEXTOF.json"] diff --git a/src/sed/config/mpes_example_config.yaml b/src/sed/config/mpes_example_config.yaml index fe81c28f..43e7db5f 100644 --- a/src/sed/config/mpes_example_config.yaml +++ b/src/sed/config/mpes_example_config.yaml @@ -310,4 +310,4 @@ nexus: definition: "NXmpes" # List containing additional input files to be handed to the pynxtools converter tool, # e.g. containing a configuration file, and additional metadata. - input_files: ["../config/NXmpes_config.json"] + input_files: ["../src/sed/config/NXmpes_config.json"] diff --git a/src/sed/core/config.py b/src/sed/core/config.py index d24b1b7b..647b1c33 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -28,7 +28,7 @@ def parse_config( folder_config: dict | str = None, user_config: dict | str = None, system_config: dict | str = None, - default_config: (dict | str) = f"{package_dir}/../../config/default.yaml", + default_config: (dict | str) = f"{package_dir}/config/default.yaml", verbose: bool = True, verify_config: bool = True, ) -> dict: diff --git a/src/sed/core/dfops.py b/src/sed/core/dfops.py index 130d602c..d92d177e 100644 --- a/src/sed/core/dfops.py +++ b/src/sed/core/dfops.py @@ -179,8 +179,8 @@ def map_columns_2d( x_column (str): The X column of the dataframe to apply mapping to. y_column (str): The Y column of the dataframe to apply mapping to. **kwds: - - *new_x_column": Name of the new x-column. Default is to overwrite the x-column. - - *new_y_column": Name of the new y-column. Default is to overwrite the y-column. + - *new_x_column*: Name of the new x-column. Default is to overwrite the x-column. + - *new_y_column*: Name of the new y-column. Default is to overwrite the y-column. Additional keyword argument are passed to the 2D mapping function. diff --git a/src/sed/dataset/dataset.py b/src/sed/dataset/dataset.py index ad445924..eda5e92a 100644 --- a/src/sed/dataset/dataset.py +++ b/src/sed/dataset/dataset.py @@ -10,6 +10,7 @@ import shutil import zipfile from datetime import datetime +from importlib.util import find_spec import requests from tqdm.auto import tqdm @@ -20,6 +21,7 @@ from sed.core.config import USER_CONFIG_PATH from sed.core.logging import setup_logging +package_dir = os.path.dirname(find_spec("sed").origin) # Configure logging logger = setup_logging("dataset") @@ -32,7 +34,7 @@ class DatasetsManager: FILENAME = NAME + ".json" json_path = {} json_path["user"] = os.path.join(USER_CONFIG_PATH, FILENAME) - json_path["module"] = os.path.join(os.path.dirname(__file__), "../../../config", FILENAME) + json_path["module"] = os.path.join(package_dir, "config", FILENAME) json_path["folder"] = "./" + FILENAME @staticmethod diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 653b97e1..e97a2764 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -15,7 +15,7 @@ from sed.dataset import DatasetsManager as dm package_dir = os.path.dirname(find_spec("sed").origin) -json_path = os.path.join(package_dir, "../../config/datasets.json") +json_path = os.path.join(package_dir, "config/datasets.json") @pytest.fixture diff --git a/tests/test_processor.py b/tests/test_processor.py index d6993144..4a64d141 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -859,7 +859,7 @@ def test_add_time_stamped_data() -> None: """Test the function to add time-stamped data""" processor = SedProcessor( folder=df_folder + "../mpes/", - config=package_dir + "/../../config/mpes_example_config.yaml", + config=package_dir + "/config/mpes_example_config.yaml", folder_config={}, user_config={}, system_config={}, @@ -1090,7 +1090,7 @@ def test_save(caplog) -> None: config = parse_config( config={"dataframe": {"tof_binning": 1}}, folder_config={}, - user_config=package_dir + "/../../config/mpes_example_config.yaml", + user_config=package_dir + "/config/mpes_example_config.yaml", system_config={}, verify_config=False, ) @@ -1099,7 +1099,7 @@ def test_save(caplog) -> None: ] = 21.0 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["Z1"] = 2450 config["metadata"]["lens_mode_config"]["6kV_kmodem4.0_30VTOF_453ns_focus.sav"]["F"] = 69.23 - config["nexus"]["input_files"] = [package_dir + "/../../config/NXmpes_config.json"] + config["nexus"]["input_files"] = [package_dir + "/config/NXmpes_config.json"] processor = SedProcessor( folder=df_folder, config=config, @@ -1147,7 +1147,7 @@ def test_save(caplog) -> None: yaml.dump({"Instrument": {"undocumented_field": "undocumented entry"}}, f) with open("temp_config.json", "w") as f: with open( - package_dir + "/../../config/NXmpes_config.json", + package_dir + "/config/NXmpes_config.json", encoding="utf-8", ) as stream: config_dict = json.load(stream) diff --git a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb index 6a4c64a5..0054aa4a 100644 --- a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb +++ b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb @@ -96,7 +96,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../config/flash_example_config.yaml')\n", + "config_file = Path('../src/sed/config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb index b672ce47..40831f8f 100644 --- a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb +++ b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb @@ -97,7 +97,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../config/flash_example_config.yaml')\n", + "config_file = Path('../src/sed/config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb index 2a10dbba..4074fc49 100644 --- a/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb +++ b/tutorial/2_conversion_pipeline_for_example_time-resolved_ARPES_data.ipynb @@ -59,7 +59,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={}, verbose=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../src/sed/config/mpes_example_config.yaml\", system_config={}, verbose=True)" ] }, { diff --git a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb index 53b58a05..d88d9d1f 100644 --- a/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb +++ b/tutorial/3_metadata_collection_and_export_to_NeXus.ipynb @@ -143,7 +143,7 @@ "outputs": [], "source": [ "# create sed processor using the config file, and collect the meta data from the files:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={}, metadata=metadata, collect_metadata=True)" + "sp = sed.SedProcessor(folder=scandir, config=\"../src/sed/config/mpes_example_config.yaml\", system_config={}, metadata=metadata, collect_metadata=True)" ] }, { diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index ad3d2e5c..51e22751 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -103,7 +103,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../config/flash_example_config.yaml')\n", + "config_file = Path('../src/sed/config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/5_sxp_workflow.ipynb b/tutorial/5_sxp_workflow.ipynb index b225a96e..4136e519 100644 --- a/tutorial/5_sxp_workflow.ipynb +++ b/tutorial/5_sxp_workflow.ipynb @@ -85,7 +85,7 @@ "outputs": [], "source": [ "# pick the default configuration file for SXP@XFEL\n", - "config_file = Path('../config/sxp_example_config.yaml')\n", + "config_file = Path('../src/sed/config/sxp_example_config.yaml')\n", "assert config_file.exists()" ] }, diff --git a/tutorial/6_binning_with_time-stamped_data.ipynb b/tutorial/6_binning_with_time-stamped_data.ipynb index 20b9e335..2c948f37 100644 --- a/tutorial/6_binning_with_time-stamped_data.ipynb +++ b/tutorial/6_binning_with_time-stamped_data.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)" + "sp = sed.SedProcessor(folder=scandir, user_config=\"../src/sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)" ] }, { diff --git a/tutorial/7_correcting_orthorhombic_symmetry.ipynb b/tutorial/7_correcting_orthorhombic_symmetry.ipynb index 47bd1530..0525bce7 100644 --- a/tutorial/7_correcting_orthorhombic_symmetry.ipynb +++ b/tutorial/7_correcting_orthorhombic_symmetry.ipynb @@ -60,7 +60,7 @@ "outputs": [], "source": [ "# create sed processor using the config file with time-stamps:\n", - "sp = sed.SedProcessor(folder=scandir, user_config=\"../config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)\n", + "sp = sed.SedProcessor(folder=scandir, user_config=\"../src/sed/config/mpes_example_config.yaml\", system_config={}, time_stamps=True, verbose=True)\n", "sp.add_jitter()" ] }, diff --git a/tutorial/8_jittering_tutorial.ipynb b/tutorial/8_jittering_tutorial.ipynb index 5b94dd08..2dd8f761 100644 --- a/tutorial/8_jittering_tutorial.ipynb +++ b/tutorial/8_jittering_tutorial.ipynb @@ -58,7 +58,7 @@ "outputs": [], "source": [ "# create sed processor using the config file:\n", - "sp = sed.SedProcessor(folder=scandir, config=\"../config/mpes_example_config.yaml\", system_config={})" + "sp = sed.SedProcessor(folder=scandir, config=\"../src/sed/config/mpes_example_config.yaml\", system_config={})" ] }, { diff --git a/tutorial/9_hextof_workflow_trXPD.ipynb b/tutorial/9_hextof_workflow_trXPD.ipynb index a50cc442..98f1f074 100644 --- a/tutorial/9_hextof_workflow_trXPD.ipynb +++ b/tutorial/9_hextof_workflow_trXPD.ipynb @@ -95,7 +95,7 @@ "outputs": [], "source": [ "# pick the default configuration file for hextof@FLASH\n", - "config_file = Path('../config/flash_example_config.yaml')\n", + "config_file = Path('../src/sed/config/flash_example_config.yaml')\n", "assert config_file.exists()" ] }, From 5f2f179acd1f405e3f8d9e70b0ed3929cdd0a022 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 22 Dec 2024 00:55:50 +0100 Subject: [PATCH 258/300] fix build scripts --- docs/scripts/build_flash_parquets.py | 2 +- docs/scripts/build_sxp_parquets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/scripts/build_flash_parquets.py b/docs/scripts/build_flash_parquets.py index 2c6c2482..004cc0af 100644 --- a/docs/scripts/build_flash_parquets.py +++ b/docs/scripts/build_flash_parquets.py @@ -6,7 +6,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -config_file = package_dir + "/../../config/flash_example_config.yaml" +config_file = package_dir + "/../../src/sed/config/flash_example_config.yaml" dataset.get("Gd_W110", root_dir="./tutorial") data_path = dataset.dir diff --git a/docs/scripts/build_sxp_parquets.py b/docs/scripts/build_sxp_parquets.py index 69915e3f..ab712dec 100644 --- a/docs/scripts/build_sxp_parquets.py +++ b/docs/scripts/build_sxp_parquets.py @@ -6,7 +6,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -config_file = package_dir + "/../../config/sxp_example_config.yaml" +config_file = package_dir + "/../../src/sed/config/sxp_example_config.yaml" dataset.get("Au_Mica", root_dir="./tutorial") data_path = dataset.dir From afd498493458835bae9077165b7175661a9e66d7 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 00:39:32 +0100 Subject: [PATCH 259/300] update pyproject and workflows --- .github/workflows/benchmark.yml | 28 +- .github/workflows/documentation.yml | 58 +- .github/workflows/linting.yml | 28 +- .github/workflows/release.yml | 167 +- .github/workflows/testing_coverage.yml | 30 +- .github/workflows/testing_multiversion.yml | 34 +- .github/workflows/update_dependencies.yml | 64 - .gitignore | 3 - MANIFEST.in | 5 + poetry.lock | 5245 -------------------- pyproject.toml | 143 +- 11 files changed, 193 insertions(+), 5612 deletions(-) delete mode 100644 .github/workflows/update_dependencies.yml create mode 100644 MANIFEST.in delete mode 100644 poetry.lock diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 16495b4c..13cb9711 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -8,6 +8,9 @@ on: paths-ignore: pyproject.toml +env: + UV_SYSTEM_PYTHON: true + jobs: benchmark: runs-on: ubuntu-latest @@ -15,8 +18,6 @@ jobs: # Check out repo and set up Python - name: Check out the repository uses: actions/checkout@v4 - with: - lfs: true - uses: tibdex/github-app-token@v1 id: generate-token @@ -24,23 +25,24 @@ jobs: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - # Use cached python and dependencies, install poetry - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main + # Setup python + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 - poetry-version: 1.8.3 + python-version: "3.10" - - name: Install project dependencies - run: poetry install + - name: Install dependencies + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install project dependencies - run: poetry install + - name: Install package + run: | + uv pip install "." # Run benchmarks - - name: Run benchmarks on python 3.8 + - name: Run benchmarks on python 3.10 run: | - poetry run pytest --full-trace --show-capture=no -sv benchmarks/benchmark_*.py + pytest --full-trace --show-capture=no -sv benchmarks/benchmark_*.py - name: Obtain git status id: status diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 7027de7e..4fe3fc8d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -11,6 +11,8 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +env: + UV_SYSTEM_PYTHON: true jobs: build: @@ -26,21 +28,24 @@ jobs: remove-android: 'true' remove-docker-images: 'true' - # Check out repo and set up Python - - name: Check out the repository - uses: actions/checkout@v4 + # Check out repo and set up Python + - uses: actions/checkout@v4 with: - lfs: true + fetch-depth: 0 - # Use cached python and dependencies, install poetry - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main + # Setup python + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 - poetry-version: 1.8.3 + python-version: "3.10" + + - name: Install dependencies + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install notebook dependencies - run: poetry install -E notebook --with docs + - name: Install package + run: | + uv pip install ".[docs,notebook]" - name: Install pandoc run: | @@ -57,32 +62,16 @@ jobs: # if: steps.cache-primes.outputs.cache-hit != 'true' run: | cd $GITHUB_WORKSPACE/docs - poetry run python scripts/download_data.py + python scripts/download_data.py - name: build parquet files run: | cd $GITHUB_WORKSPACE/docs - poetry run python scripts/build_flash_parquets.py - poetry run python scripts/build_sxp_parquets.py - - - name: Change version for develop build - if: startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main' - run: | - VERSION=`sed -n 's/^version = "\(.*\)".*/\1/p' $GITHUB_WORKSPACE/pyproject.toml` - MOD_VERSION=$VERSION".dev0" - echo $MOD_VERSION - sed -i "s/^version = \"$VERSION\"/version = \"$MOD_VERSION\"/" $GITHUB_WORKSPACE/pyproject.toml - - - name: Change version for release build - if: startsWith(github.ref, 'refs/tags/') - run: | - OLD_VERSION=`sed -n 's/^version = "\(.*\)".*/\1/p' $GITHUB_WORKSPACE/pyproject.toml` - NEW_VERSION=`echo ${GITHUB_REF#refs/tags/} | sed -n 's/^v\(.*\)/\1/p'` - echo $NEW_VERSION - sed -i "s/^version = \"$OLD_VERSION\"/version = \"$NEW_VERSION\"/" $GITHUB_WORKSPACE/pyproject.toml + python scripts/build_flash_parquets.py + python scripts/build_sxp_parquets.py - name: build Sphinx docs - run: poetry run sphinx-build -b html $GITHUB_WORKSPACE/docs $GITHUB_WORKSPACE/_build + run: sphinx-build -b html $GITHUB_WORKSPACE/docs $GITHUB_WORKSPACE/_build - name: Upload artifact uses: actions/upload-artifact@v4 @@ -96,17 +85,12 @@ jobs: needs: build steps: - name: Checkout docs repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/docs token: ${{ secrets.GITHUB_TOKEN }} path: 'docs-repo' - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - name: Setup SSH uses: webfactory/ssh-agent@v0.9.0 with: diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 89c92048..2539e1cc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -6,34 +6,42 @@ on: paths-ignore: pyproject.toml +env: + UV_SYSTEM_PYTHON: true + jobs: lint: runs-on: ubuntu-latest steps: # Check out repo and set up Python - uses: actions/checkout@v4 - with: - lfs: true - # Use cached python and dependencies, install poetry - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main + # Setup python + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 - poetry-version: 1.8.3 + python-version: "3.10" + + - name: Install dependencies + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install package + run: | + uv pip install ".[dev]" # Linting steps, execute all linters even if one fails - name: ruff run: - poetry run ruff src/sed tests + ruff src/sed tests - name: ruff formatting if: ${{ always() }} run: - poetry run ruff format --check src/sed tests + ruff format --check src/sed tests - name: mypy if: ${{ always() }} run: - poetry run mypy src/sed tests + mypy src/sed tests - name: spellcheck if: ${{ always() }} uses: streetsidesoftware/cspell-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce492666..842ed8fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,10 @@ name: Publish to PyPI -# Workflow runs prerelease job on every push to main branch -# and a release job on every tag push. -# A publish job is executed on every successful prerelease or release job -# And if publish is successful, the version is also updated in the pyproject.toml file and pushed to main branch - # Workflow does not trigger itself as it only changes pyproject.toml, which is not in paths for this workflow +# Workflow runs a release job on every published tag. # The package is distributed as sed-processor on: - push: - branches: - - main - tags: - - v[0-9]+.[0-9]+.[0-9]+ - paths: - - src/sed/**/* - - .github/workflows/release.yml + release: + types: [published] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -24,149 +14,36 @@ concurrency: group: "release" cancel-in-progress: false -jobs: - prerelease: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - lfs: true - path: 'sed-processor' - - - name: "Setup Python, Poetry and Dependencies" - uses: zain-sohail/action-setup-cache-python-poetry@main - with: - python-version: 3.9 - poetry-version: 1.8.3 - working-directory: sed-processor - - - name: Change to distribution name in toml file - run: | - cd sed-processor - sed -i 's/name = "sed"/name = "sed-processor"/' pyproject.toml - - - name: bump pre-release version - id: version - working-directory: sed-processor - run: | - VERSION=$(poetry version -s prerelease) - echo "version=$VERSION" >> $GITHUB_OUTPUT - poetry build - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: dist - path: sed-processor/dist - - - name: Upload pyproject.toml - uses: actions/upload-artifact@v4 - with: - name: pyproject - path: sed-processor/pyproject.toml +env: + UV_SYSTEM_PYTHON: true +jobs: release: - if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - lfs: true - path: 'sed-processor' - - - name: "Setup Python, Poetry and Dependencies" - uses: zain-sohail/action-setup-cache-python-poetry@main - with: - python-version: 3.9 - poetry-version: 1.8.3 - working-directory: sed-processor - - - name: Change to distribution name in toml file - run: | - cd sed-processor - sed -i 's/name = "sed"/name = "sed-processor"/' pyproject.toml - - - name: Bump release version and build - id: version - working-directory: sed-processor - run: | - VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-.*//') - echo "version=$VERSION" >> $GITHUB_OUTPUT - poetry version $VERSION - poetry build - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: dist - path: sed-processor/dist - - - name: Upload pyproject.toml - uses: actions/upload-artifact@v4 - with: - name: pyproject - path: sed-processor/pyproject.toml - - publish: - needs: [prerelease, release] - if: always() && (needs.prerelease.result == 'success' || needs.release.result == 'success') + name: Upload release to PyPI runs-on: ubuntu-latest - outputs: - version: ${{ needs.prerelease.outputs.version || needs.release.outputs.version }} environment: - name: pypi - url: https://pypi.org/p/sed-processor + name: test-pypi + url: https://test-pypi.org/p/sed-processor permissions: - id-token: write - - steps: - - name: Download a single artifact - uses: actions/download-artifact@v4 - with: - name: dist - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: . + id-token: write - bump-version: - needs: publish - if: always() && (needs.publish.result == 'success') - runs-on: ubuntu-latest steps: - - name: Generate a token - id: generate_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: actions/checkout@v4 with: - lfs: true - token: ${{ steps.generate_token.outputs.token }} + fetch-depth: 0 + path: 'sed-processor' - - name: Download pyproject.toml - uses: actions/download-artifact@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - name: pyproject + python-version: "3.x" - - name: Commit files + - name: Install dependencies run: | - cd sed/ - git config --local user.email "bump[bot]@users.noreply.github.com" - git config --local user.name "bump[bot]" - git add $GITHUB_WORKSPACE/pyproject.toml - git commit -m "bump version to ${{ needs.publish.outputs.version }}" + curl -LsSf https://astral.sh/uv/install.sh | sh + uv pip install build - - name: Push changes - uses: ad-m/github-push-action@master - with: - github_token: ${{ steps.generate_token.outputs.token }} - branch: main + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + #with: + # packages-dir: . diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index e89a46a4..f8d2cc02 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -10,30 +10,34 @@ on: branches: - main +env: + UV_SYSTEM_PYTHON: true + jobs: pytest: runs-on: ubuntu-latest steps: # Check out repo and set up Python - - name: Check out the repository - uses: actions/checkout@v4 - with: - lfs: true + - uses: actions/checkout@v4 - # Use cached python and dependencies, install poetry - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main + # Setup python + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 - poetry-version: 1.8.3 + python-version: "3.10" - - name: Install project dependencies - run: poetry install + - name: Install dependencies + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install package + run: | + uv pip install ".[dev]" # Run pytest with coverage report, saving to xml - - name: Run tests on python 3.9 + - name: Run tests on python 3.10 run: | - poetry run pytest --cov --cov-report xml:cobertura.xml --full-trace --show-capture=no -sv -n auto tests/ + pytest --cov --cov-report xml:cobertura.xml --full-trace --show-capture=no -sv -n auto tests/ # Take report and upload to coveralls - name: Coveralls diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index f0efc943..b46f9778 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -2,36 +2,42 @@ name: Unit Tests on: + schedule: + - cron: '0 1 * * 1' workflow_dispatch: push: branches: [ main, v1_feature_branch ] paths-ignore: pyproject.toml +env: + UV_SYSTEM_PYTHON: true + jobs: pytest: + runs-on: ubuntu-latest # Using matrix strategy strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - runs-on: ubuntu-latest + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: # Check out repo and set up Python - - name: Check out the repository - uses: actions/checkout@v4 - with: - lfs: true + - uses: actions/checkout@v4 - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: - python-version: ${{matrix.python-version}} - poetry-version: 1.8.3 + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install project dependencies - run: poetry install + - name: Install package + run: | + uv pip install ".[dev]" - # Use cached python and dependencies, install poetry - name: Run tests on python ${{matrix.python-version}} run: | - poetry run pytest --full-trace --show-capture=no -sv -n auto tests/ + pytest --full-trace --show-capture=no -sv -n auto tests/ diff --git a/.github/workflows/update_dependencies.yml b/.github/workflows/update_dependencies.yml deleted file mode 100644 index 302d3295..00000000 --- a/.github/workflows/update_dependencies.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Update dependencies in poetry lockfile - -on: - schedule: - - cron: '0 1 * * 1' - workflow_dispatch: - push: - branches: main - paths: - - .github/workflows/update_dependencies.yml - -jobs: - update_dependencies: - runs-on: ubuntu-latest - steps: - # Check out repo and set up Python - - uses: actions/checkout@v4 - with: - lfs: true - - - uses: tibdex/github-app-token@v1 - id: generate-token - with: - app_id: ${{ secrets.APP_ID }} - private_key: ${{ secrets.APP_PRIVATE_KEY }} - - # Use cached python and dependencies, install poetry - - name: "Setup Python, Poetry and Dependencies" - uses: packetcoders/action-setup-cache-python-poetry@main - with: - python-version: 3.9 - poetry-version: 1.8.3 - - # update poetry lockfile - - name: "Update poetry lock file" - id: update - run: | - poetry self update - exec 5>&1 - UPDATE_OUTPUT=$(poetry update|tee >(cat - >&5)) - echo "UPDATE_OUTPUT<> $GITHUB_OUTPUT - echo "$UPDATE_OUTPUT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Obtain git status - id: status - run: | - exec 5>&1 - STATUS=$(git status|tee >(cat - >&5)) - echo "STATUS<> $GITHUB_OUTPUT - echo "$STATUS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - # create pull request if necessary - - name: "Create Pull Request" - uses: peter-evans/create-pull-request@v5 - if: ${{ contains(steps.status.outputs.STATUS, 'poetry.lock') || contains(steps.status.outputs.STATUS, 'requirements.txt')}} - with: - token: ${{ steps.generate-token.outputs.token }} - commit-message: Update dependencies - title: "Update dependencies" - body: | - Dependency updates using Poetry: - ${{ steps.update.outputs.UPDATE_OUTPUT }} diff --git a/.gitignore b/.gitignore index fe0029c4..8cd12dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -149,6 +149,3 @@ dmypy.json # IDE stuff \.vscode - -# poetry local config -poetry.toml diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..d9ea7561 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +prune * +exclude * +recursive-include src/sed *.py +include pyproject.toml README.md +graft src/sed/config/ diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 2c037285..00000000 --- a/poetry.lock +++ /dev/null @@ -1,5245 +0,0 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. - -[[package]] -name = "accessible-pygments" -version = "0.0.5" -description = "A collection of accessible pygments styles" -optional = false -python-versions = ">=3.9" -files = [ - {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, - {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, -] - -[package.dependencies] -pygments = ">=1.5" - -[package.extras] -dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] -tests = ["hypothesis", "pytest"] - -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.7.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "anytree" -version = "2.12.1" -description = "Powerful and Lightweight Python Tree Data Structure with various plugins" -optional = false -python-versions = ">=3.7.2,<4" -files = [ - {file = "anytree-2.12.1-py3-none-any.whl", hash = "sha256:5ea9e61caf96db1e5b3d0a914378d2cd83c269dfce1fb8242ce96589fa3382f0"}, - {file = "anytree-2.12.1.tar.gz", hash = "sha256:244def434ccf31b668ed282954e5d315b4e066c4940b94aff4a7962d85947830"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "appnope" -version = "0.1.4" -description = "Disable App Nap on macOS >= 10.9" -optional = true -python-versions = ">=3.6" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -description = "Argon2 for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[package.dependencies] -argon2-cffi-bindings = "*" - -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -description = "Low-level CFFI bindings for Argon2" -optional = false -python-versions = ">=3.6" -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, -] - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - -[[package]] -name = "ase" -version = "3.24.0" -description = "Atomic Simulation Environment" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ase-3.24.0-py3-none-any.whl", hash = "sha256:974922df87ef4ec8cf1140359a55ab4c4dc55c38e26876bdd9c00968da1f463c"}, - {file = "ase-3.24.0.tar.gz", hash = "sha256:9acc93d6daaf48cd27b844c56f8bf49428b9db0542faa3cc30d9d5b8e1842195"}, -] - -[package.dependencies] -matplotlib = ">=3.3.4" -numpy = ">=1.19.5" -scipy = ">=1.6.0" - -[package.extras] -docs = ["pillow", "sphinx", "sphinx_rtd_theme"] -spglib = ["spglib (>=1.9)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.1.0)"] - -[[package]] -name = "asteval" -version = "1.0.5" -description = "Safe, minimalistic evaluator of python expression using ast module" -optional = false -python-versions = ">=3.8" -files = [ - {file = "asteval-1.0.5-py3-none-any.whl", hash = "sha256:082b95312578affc8a6d982f7d92b7ac5de05634985c87e7eedd3188d31149fa"}, - {file = "asteval-1.0.5.tar.gz", hash = "sha256:bac3c8dd6d2b789e959cfec9bb296fb8338eec066feae618c462132701fbc665"}, -] - -[package.extras] -all = ["asteval[dev,doc,test]"] -dev = ["build", "twine"] -doc = ["Sphinx"] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "astropy" -version = "6.0.1" -description = "Astronomy and astrophysics core library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "astropy-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445"}, - {file = "astropy-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a"}, - {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466"}, - {file = "astropy-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88"}, - {file = "astropy-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1"}, - {file = "astropy-6.0.1-cp310-cp310-win32.whl", hash = "sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1"}, - {file = "astropy-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c"}, - {file = "astropy-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23"}, - {file = "astropy-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292"}, - {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e"}, - {file = "astropy-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363"}, - {file = "astropy-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4"}, - {file = "astropy-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5"}, - {file = "astropy-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27"}, - {file = "astropy-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97"}, - {file = "astropy-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f"}, - {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc"}, - {file = "astropy-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165"}, - {file = "astropy-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4"}, - {file = "astropy-6.0.1-cp312-cp312-win32.whl", hash = "sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723"}, - {file = "astropy-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3"}, - {file = "astropy-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9"}, - {file = "astropy-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6"}, - {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08"}, - {file = "astropy-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf"}, - {file = "astropy-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0"}, - {file = "astropy-6.0.1-cp39-cp39-win32.whl", hash = "sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563"}, - {file = "astropy-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23"}, - {file = "astropy-6.0.1.tar.gz", hash = "sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c"}, -] - -[package.dependencies] -astropy-iers-data = ">=0.2024.2.26.0.28.55" -numpy = ">=1.22,<2" -packaging = ">=19.0" -pyerfa = ">=2.0.1.1" -PyYAML = ">=3.13" - -[package.extras] -all = ["asdf-astropy (>=0.3)", "astropy[recommended]", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2023.4.0)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2023.4.0)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] -docs = ["Jinja2 (>=3.1.3)", "astropy[recommended]", "pytest (>=7.0)", "sphinx", "sphinx-astropy[confv2] (>=1.9.1)", "sphinx-changelog (>=1.2.0)", "sphinx-design", "tomli"] -recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] -test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"] -test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"] - -[[package]] -name = "astropy-iers-data" -version = "0.2024.12.30.0.33.36" -description = "IERS Earth Rotation and Leap Second tables for the astropy core package" -optional = false -python-versions = ">=3.8" -files = [ - {file = "astropy_iers_data-0.2024.12.30.0.33.36-py3-none-any.whl", hash = "sha256:dbf3e85b65842ce1871dc71f6f255b298805d93362e7c16fe1801f7ca4ae39b3"}, - {file = "astropy_iers_data-0.2024.12.30.0.33.36.tar.gz", hash = "sha256:5cb8dc129c2a39d5a62fa345e166833976050a78d85e99d93ef247e310703f12"}, -] - -[package.extras] -docs = ["pytest"] -test = ["hypothesis", "pytest", "pytest-remotedata"] - -[[package]] -name = "asttokens" -version = "3.0.0" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, - {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, -] - -[package.extras] -astroid = ["astroid (>=2,<4)"] -test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "async-lru" -version = "2.0.4" -description = "Simple LRU cache for asyncio" -optional = true -python-versions = ">=3.8" -files = [ - {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, - {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "attrs" -version = "24.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "babel" -version = "2.16.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bleach" -version = "6.2.0" -description = "An easy safelist-based HTML-sanitizing tool." -optional = false -python-versions = ">=3.9" -files = [ - {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, - {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, -] - -[package.dependencies] -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.5)"] - -[[package]] -name = "bokeh" -version = "3.4.3" -description = "Interactive plots and applications in the browser from Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "bokeh-3.4.3-py3-none-any.whl", hash = "sha256:c6f33817f866fc67fbeb5df79cd13a8bb592c05c591f3fd7f4f22b824f7afa01"}, - {file = "bokeh-3.4.3.tar.gz", hash = "sha256:b7c22fb0f7004b04f12e1b7b26ee0269a26737a08ded848fb58f6a34ec1eb155"}, -] - -[package.dependencies] -contourpy = ">=1.2" -Jinja2 = ">=2.9" -numpy = ">=1.16" -packaging = ">=16.8" -pandas = ">=1.2" -pillow = ">=7.1.0" -PyYAML = ">=3.10" -tornado = ">=6.2" -xyzservices = ">=2021.09.1" - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "click-default-group" -version = "1.2.4" -description = "click_default_group" -optional = false -python-versions = ">=2.7" -files = [ - {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, - {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, -] - -[package.dependencies] -click = "*" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "cloudpickle" -version = "3.1.0" -description = "Pickler class to extend the standard pickle.Pickler functionality" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cloudpickle-3.1.0-py3-none-any.whl", hash = "sha256:fe11acda67f61aaaec473e3afe030feb131d78a43461b718185363384f1ba12e"}, - {file = "cloudpickle-3.1.0.tar.gz", hash = "sha256:81a929b6e3c7335c863c771d673d105f02efdb89dfaba0c90495d1c64796601b"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "comm" -version = "0.2.2" -description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -optional = false -python-versions = ">=3.8" -files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, -] - -[package.dependencies] -traitlets = ">=4" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -optional = false -python-versions = "*" -files = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "contourpy" -version = "1.3.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.9" -files = [ - {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, - {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, - {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, - {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, - {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, - {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, - {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, - {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, - {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, - {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, - {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, - {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, - {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"}, -] - -[package.dependencies] -numpy = ">=1.23" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] - -[[package]] -name = "coverage" -version = "7.6.10" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cycler" -version = "0.12.1" -description = "Composable style cycles" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[package.extras] -docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] -tests = ["pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "dask" -version = "2024.7.1" -description = "Parallel PyData with Task Scheduling" -optional = false -python-versions = ">=3.9" -files = [ - {file = "dask-2024.7.1-py3-none-any.whl", hash = "sha256:dd046840050376c317de90629db5c6197adda820176cf3e2df10c3219d11951f"}, - {file = "dask-2024.7.1.tar.gz", hash = "sha256:dbaef2d50efee841a9d981a218cfeb50392fc9a95e0403b6d680450e4f50d531"}, -] - -[package.dependencies] -click = ">=8.1" -cloudpickle = ">=1.5.0" -fsspec = ">=2021.09.0" -importlib-metadata = {version = ">=4.13.0", markers = "python_version < \"3.12\""} -packaging = ">=20.0" -partd = ">=1.4.0" -pyyaml = ">=5.3.1" -toolz = ">=0.10.0" - -[package.extras] -array = ["numpy (>=1.21)"] -complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] -dataframe = ["dask-expr (>=1.1,<1.2)", "dask[array]", "pandas (>=2.0)"] -diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.7.1)"] -test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] - -[[package]] -name = "debugpy" -version = "1.8.11" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = true -python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, - {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, - {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, - {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, - {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, - {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, - {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, - {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, - {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, - {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, - {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, - {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, - {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, - {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, - {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, - {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, - {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, - {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, - {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, - {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, - {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, - {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, - {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, - {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, - {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, - {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "dill" -version = "0.3.9" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, - {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "distlib" -version = "0.3.9" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, - {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, -] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "executing" -version = "2.1.0" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.8" -files = [ - {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, - {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastdtw" -version = "0.3.4" -description = "Dynamic Time Warping (DTW) algorithm with an O(N) time and memory complexity." -optional = false -python-versions = "*" -files = [ - {file = "fastdtw-0.3.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:28918c163dce9e736e09252a02073fce712bc4c7aa18f2a45d882cca84da2dbb"}, - {file = "fastdtw-0.3.4.tar.gz", hash = "sha256:2350fa6ec36bcad186eaf81f46eff35181baf04e324f522de8aeb43d0243f64f"}, -] - -[package.dependencies] -numpy = "*" - -[[package]] -name = "fastjsonschema" -version = "2.21.1" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, - {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "filelock" -version = "3.16.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[package]] -name = "fonttools" -version = "4.55.3" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0"}, - {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f"}, - {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841"}, - {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674"}, - {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276"}, - {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5"}, - {file = "fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261"}, - {file = "fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5"}, - {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e"}, - {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b"}, - {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90"}, - {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0"}, - {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b"}, - {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765"}, - {file = "fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f"}, - {file = "fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72"}, - {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35"}, - {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c"}, - {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7"}, - {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314"}, - {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427"}, - {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a"}, - {file = "fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07"}, - {file = "fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54"}, - {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29"}, - {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4"}, - {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca"}, - {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b"}, - {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048"}, - {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe"}, - {file = "fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628"}, - {file = "fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b"}, - {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3"}, - {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d"}, - {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa"}, - {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e"}, - {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de"}, - {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926"}, - {file = "fonttools-4.55.3-cp38-cp38-win32.whl", hash = "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b"}, - {file = "fonttools-4.55.3-cp38-cp38-win_amd64.whl", hash = "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56"}, - {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af"}, - {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831"}, - {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02"}, - {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4"}, - {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd"}, - {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"}, - {file = "fonttools-4.55.3-cp39-cp39-win32.whl", hash = "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851"}, - {file = "fonttools-4.55.3-cp39-cp39-win_amd64.whl", hash = "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d"}, - {file = "fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977"}, - {file = "fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "fqdn" -version = "1.5.1" -description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -optional = false -python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" -files = [ - {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, - {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, -] - -[[package]] -name = "fsspec" -version = "2024.12.0" -description = "File-system specification" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fsspec-2024.12.0-py3-none-any.whl", hash = "sha256:b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2"}, - {file = "fsspec-2024.12.0.tar.gz", hash = "sha256:670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] -tqdm = ["tqdm"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = true -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "h5grove" -version = "2.3.0" -description = "Core utilities to serve HDF5 file contents" -optional = false -python-versions = ">=3.8" -files = [ - {file = "h5grove-2.3.0-py3-none-any.whl", hash = "sha256:7000a5aa64a6d77997ad18296552ec4def62d4e24aa05219b59c535233017fdf"}, - {file = "h5grove-2.3.0.tar.gz", hash = "sha256:8d438f2a4616d64b176e6ff352d23b39027b9f1ab2c9169fcfd6f5f1caca8728"}, -] - -[package.dependencies] -h5py = ">=3" -numpy = "*" -orjson = "*" -tifffile = "*" -typing-extensions = "*" - -[package.extras] -dev = ["black", "bump2version", "check-manifest", "eval-type-backport", "flake8", "h5grove[fastapi]", "h5grove[flask]", "h5grove[tornado]", "httpx (>=0.23)", "invoke", "mypy", "myst-parser", "pytest", "pytest-benchmark", "pytest-cov", "pytest-tornado", "sphinx", "sphinx-argparse", "sphinx-autobuild", "types-contextvars", "types-dataclasses", "types-orjson", "types-setuptools"] -fastapi = ["fastapi", "pydantic (>2)", "pydantic-settings", "uvicorn"] -flask = ["Flask", "Flask-Compress", "Flask-Cors"] -tornado = ["tornado"] - -[[package]] -name = "h5py" -version = "3.12.1" -description = "Read and write HDF5 files from Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, - {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, - {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, - {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, - {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, - {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, - {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, - {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, - {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, - {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, - {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, - {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, - {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, - {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, - {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, - {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, - {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, - {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, - {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, - {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, - {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, - {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, - {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, - {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, - {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, - {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, -] - -[package.dependencies] -numpy = ">=1.19.3" - -[[package]] -name = "hdf5plugin" -version = "5.0.0" -description = "HDF5 Plugins for Windows, MacOS, and Linux" -optional = false -python-versions = ">=3.8" -files = [ - {file = "hdf5plugin-5.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:8f696fcfd8c05b574e98180580e6d28428582cb9c7dd62b17c41ce3bdd5c5994"}, - {file = "hdf5plugin-5.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6793420f5c0bc753e925ef47fac74e491f8aaf27bfa6c61fce5fccaf4cd8e767"}, - {file = "hdf5plugin-5.0.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b34b4e1d71ed47fdd080fce30d9fa9b043c9263385584e8006903c0c10eae1"}, - {file = "hdf5plugin-5.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5f3e9cb4448841d07dd9d9258132b7eb900b38f8c49e899efe4050834757e6"}, - {file = "hdf5plugin-5.0.0-py3-none-win_amd64.whl", hash = "sha256:9bded0f5536471ace7855bd881762de1125586af1162001c39b8e899b89c47e2"}, - {file = "hdf5plugin-5.0.0.tar.gz", hash = "sha256:3bcc5c4f523953fe020a220c7b1b307c62066e39fdbdcd904fa2268db80e9dbb"}, -] - -[package.dependencies] -h5py = ">=3.0.0" - -[package.extras] -doc = ["ipython", "nbsphinx", "sphinx", "sphinx-rtd-theme"] -test = ["blosc2 (>=2.5.1)", "blosc2-grok (>=0.2.2)"] - -[[package]] -name = "httpcore" -version = "1.0.7" -description = "A minimal low-level HTTP client." -optional = true -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = true -python-versions = ">=3.8" -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "identify" -version = "2.6.4" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "identify-2.6.4-py2.py3-none-any.whl", hash = "sha256:993b0f01b97e0568c179bb9196391ff391bfb88a99099dbf5ce392b68f42d0af"}, - {file = "identify-2.6.4.tar.gz", hash = "sha256:285a7d27e397652e8cafe537a6cc97dd470a970f48fb2e9d979aa38eae5513ac"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imageio" -version = "2.36.1" -description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." -optional = false -python-versions = ">=3.9" -files = [ - {file = "imageio-2.36.1-py3-none-any.whl", hash = "sha256:20abd2cae58e55ca1af8a8dcf43293336a59adf0391f1917bf8518633cfc2cdf"}, - {file = "imageio-2.36.1.tar.gz", hash = "sha256:e4e1d231f47f9a9e16100b0f7ce1a86e8856fb4d1c0fa2c4365a316f1746be62"}, -] - -[package.dependencies] -numpy = "*" -pillow = ">=8.3.2" - -[package.extras] -all-plugins = ["astropy", "av", "imageio-ffmpeg", "numpy (>2)", "pillow-heif", "psutil", "rawpy", "tifffile"] -all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] -build = ["wheel"] -dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] -docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] -ffmpeg = ["imageio-ffmpeg", "psutil"] -fits = ["astropy"] -full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpy (>2)", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "rawpy", "sphinx (<6)", "tifffile", "wheel"] -gdal = ["gdal"] -itk = ["itk"] -linting = ["black", "flake8"] -pillow-heif = ["pillow-heif"] -pyav = ["av"] -rawpy = ["numpy (>2)", "rawpy"] -test = ["fsspec[github]", "pytest", "pytest-cov"] -tifffile = ["tifffile"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "ipykernel" -version = "6.29.5" -description = "IPython Kernel for Jupyter" -optional = true -python-versions = ">=3.8" -files = [ - {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, - {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -comm = ">=0.1.1" -debugpy = ">=1.6.5" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=24" -tornado = ">=6.1" -traitlets = ">=5.4.0" - -[package.extras] -cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] -pyqt5 = ["pyqt5"] -pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipympl" -version = "0.9.5" -description = "Matplotlib Jupyter Extension" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ipympl-0.9.5-py3-none-any.whl", hash = "sha256:ff14a357f43552ec92582eb8b2f76b844b469aa2fa76833f15ead7fa584c19fc"}, - {file = "ipympl-0.9.5.tar.gz", hash = "sha256:b702db4bd1138cee21ab3c731e1c1bc5c3efbfa81b9b42a1820f79122d946dc8"}, -] - -[package.dependencies] -ipython = "<9" -ipywidgets = ">=7.6.0,<9" -matplotlib = ">=3.4.0,<4" -numpy = "*" -pillow = "*" -traitlets = "<6" - -[package.extras] -docs = ["myst-nb", "sphinx (>=1.5)", "sphinx-book-theme", "sphinx-copybutton", "sphinx-thebe", "sphinx-togglebutton"] - -[[package]] -name = "ipython" -version = "8.18.1" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] - -[[package]] -name = "ipywidgets" -version = "8.1.5" -description = "Jupyter interactive widgets" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, - {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, -] - -[package.dependencies] -comm = ">=0.1.3" -ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.12,<3.1.0" -traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.12,<4.1.0" - -[package.extras] -test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] - -[[package]] -name = "isoduration" -version = "20.11.0" -description = "Operations with ISO 8601 durations" -optional = false -python-versions = ">=3.7" -files = [ - {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, - {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, -] - -[package.dependencies] -arrow = ">=0.15.0" - -[[package]] -name = "jedi" -version = "0.19.2" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, - {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, -] - -[package.dependencies] -parso = ">=0.8.4,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.5" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "joblib" -version = "1.4.2" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, - {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, -] - -[[package]] -name = "json5" -version = "0.10.0" -description = "A Python implementation of the JSON5 data format." -optional = true -python-versions = ">=3.8.0" -files = [ - {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, - {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, -] - -[package.extras] -dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} -rpds-py = ">=0.7.1" -uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=24.6.0", optional = true, markers = "extra == \"format-nongpl\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-specifications" -version = "2024.10.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.9" -files = [ - {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, - {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "jupyter" -version = "1.1.1" -description = "Jupyter metapackage. Install all the Jupyter components in one go." -optional = true -python-versions = "*" -files = [ - {file = "jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83"}, - {file = "jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a"}, -] - -[package.dependencies] -ipykernel = "*" -ipywidgets = "*" -jupyter-console = "*" -jupyterlab = "*" -nbconvert = "*" -notebook = "*" - -[[package]] -name = "jupyter-client" -version = "8.6.3" -description = "Jupyter protocol implementation and client libraries" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, - {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = ">=5.3" - -[package.extras] -docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] - -[[package]] -name = "jupyter-console" -version = "6.6.3" -description = "Jupyter terminal console" -optional = true -python-versions = ">=3.7" -files = [ - {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, - {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, -] - -[package.dependencies] -ipykernel = ">=6.14" -ipython = "*" -jupyter-client = ">=7.0.0" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -prompt-toolkit = ">=3.0.30" -pygments = "*" -pyzmq = ">=17" -traitlets = ">=5.4" - -[package.extras] -test = ["flaky", "pexpect", "pytest"] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -description = "Jupyter core package. A base package on which Jupyter projects rely." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, -] - -[package.dependencies] -platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = ">=5.3" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-events" -version = "0.11.0" -description = "Jupyter Event System library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "jupyter_events-0.11.0-py3-none-any.whl", hash = "sha256:36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf"}, - {file = "jupyter_events-0.11.0.tar.gz", hash = "sha256:c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90"}, -] - -[package.dependencies] -jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} -python-json-logger = ">=2.0.4" -pyyaml = ">=5.3" -referencing = "*" -rfc3339-validator = "*" -rfc3986-validator = ">=0.1.1" -traitlets = ">=5.3" - -[package.extras] -cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8)", "sphinxcontrib-spelling"] -test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] - -[[package]] -name = "jupyter-lsp" -version = "2.2.5" -description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, - {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-server = ">=1.1.2" - -[[package]] -name = "jupyter-server" -version = "2.15.0" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -optional = false -python-versions = ">=3.9" -files = [ - {file = "jupyter_server-2.15.0-py3-none-any.whl", hash = "sha256:872d989becf83517012ee669f09604aa4a28097c0bd90b2f424310156c2cdae3"}, - {file = "jupyter_server-2.15.0.tar.gz", hash = "sha256:9d446b8697b4f7337a1b7cdcac40778babdd93ba614b6d68ab1c0c918f1c4084"}, -] - -[package.dependencies] -anyio = ">=3.1.0" -argon2-cffi = ">=21.1" -jinja2 = ">=3.0.3" -jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.11.0" -jupyter-server-terminals = ">=0.4.4" -nbconvert = ">=6.4.4" -nbformat = ">=5.3.0" -overrides = ">=5.0" -packaging = ">=22.0" -prometheus-client = ">=0.9" -pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} -pyzmq = ">=24" -send2trash = ">=1.8.2" -terminado = ">=0.8.3" -tornado = ">=6.2.0" -traitlets = ">=5.6.0" -websocket-client = ">=1.7" - -[package.extras] -docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] -test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] - -[[package]] -name = "jupyter-server-terminals" -version = "0.5.3" -description = "A Jupyter Server Extension Providing Terminals." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, - {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, -] - -[package.dependencies] -pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} -terminado = ">=0.8.3" - -[package.extras] -docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] -test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] - -[[package]] -name = "jupyterlab" -version = "4.3.4" -description = "JupyterLab computational environment" -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyterlab-4.3.4-py3-none-any.whl", hash = "sha256:b754c2601c5be6adf87cb5a1d8495d653ffb945f021939f77776acaa94dae952"}, - {file = "jupyterlab-4.3.4.tar.gz", hash = "sha256:f0bb9b09a04766e3423cccc2fc23169aa2ffedcdf8713e9e0fb33cac0b6859d0"}, -] - -[package.dependencies] -async-lru = ">=1.0.0" -httpx = ">=0.25.0" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -ipykernel = ">=6.5.0" -jinja2 = ">=3.0.3" -jupyter-core = "*" -jupyter-lsp = ">=2.0.0" -jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2" -packaging = "*" -setuptools = ">=40.8.0" -tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} -tornado = ">=6.2.0" -traitlets = "*" - -[package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"] -docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"] -test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] - -[[package]] -name = "jupyterlab-h5web" -version = "12.3.0" -description = "A JupyterLab extension to explore and visualize HDF5 file contents." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_h5web-12.3.0-py3-none-any.whl", hash = "sha256:75aafa5c622cd5882161ea4865acdefdbfa59eecea57a1bf3fa6d600297554af"}, - {file = "jupyterlab_h5web-12.3.0.tar.gz", hash = "sha256:117686d4a5185c03f86d6ba1e5da9c51be170d5bc22f5dc930cb5d48a1d46150"}, -] - -[package.dependencies] -h5grove = "2.3.0" -h5py = ">=3.5" -hdf5plugin = {version = "*", optional = true, markers = "extra == \"full\""} -jupyter-server = ">=1.6,<3" - -[package.extras] -full = ["hdf5plugin"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -description = "Pygments theme using JupyterLab CSS variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, - {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, -] - -[[package]] -name = "jupyterlab-server" -version = "2.27.3" -description = "A set of server components for JupyterLab and JupyterLab like applications." -optional = true -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4"}, - {file = "jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4"}, -] - -[package.dependencies] -babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.3" -json5 = ">=0.9.0" -jsonschema = ">=4.18.0" -jupyter-server = ">=1.21,<3" -packaging = ">=21.3" -requests = ">=2.31" - -[package.extras] -docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] -openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] -test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] - -[[package]] -name = "jupyterlab-widgets" -version = "3.0.13" -description = "Jupyter interactive widgets for JupyterLab" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, - {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.7" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.8" -files = [ - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, - {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, - {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, - {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, - {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, - {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, - {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, - {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, - {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, - {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, - {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, - {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, - {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, - {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, - {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, - {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, - {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, - {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, - {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, - {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, - {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, - {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, - {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, - {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, - {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, - {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, - {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, - {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, -] - -[[package]] -name = "lazy-loader" -version = "0.4" -description = "Makes it easy to load subpackages and functions on demand." -optional = false -python-versions = ">=3.7" -files = [ - {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, - {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -dev = ["changelist (==0.5)"] -lint = ["pre-commit (==3.7.0)"] -test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] - -[[package]] -name = "llvmlite" -version = "0.43.0" -description = "lightweight wrapper around basic LLVM functionality" -optional = false -python-versions = ">=3.9" -files = [ - {file = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"}, - {file = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"}, - {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead"}, - {file = "llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a"}, - {file = "llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed"}, - {file = "llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98"}, - {file = "llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57"}, - {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2"}, - {file = "llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749"}, - {file = "llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91"}, - {file = "llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7"}, - {file = "llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7"}, - {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f"}, - {file = "llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844"}, - {file = "llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9"}, - {file = "llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c"}, - {file = "llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8"}, - {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a"}, - {file = "llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867"}, - {file = "llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4"}, - {file = "llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5"}, -] - -[[package]] -name = "lmfit" -version = "1.3.2" -description = "Least-Squares Minimization with Bounds and Constraints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "lmfit-1.3.2-py3-none-any.whl", hash = "sha256:2b834f054cd7a5172f3b431345b292e5d95ea387d6f96d60ad35a11b8efee6ac"}, - {file = "lmfit-1.3.2.tar.gz", hash = "sha256:31beeae1f027c1b8c14dcd7f2e8488a80b75fb389e77fca677549bdc2fe597bb"}, -] - -[package.dependencies] -asteval = ">=1.0" -dill = ">=0.3.4" -numpy = ">=1.19" -scipy = ">=1.6" -uncertainties = ">=3.2.2" - -[package.extras] -all = ["lmfit[dev,doc,test]"] -dev = ["build", "check-wheel-contents", "flake8-pyproject", "pre-commit", "twine"] -doc = ["Pillow", "Sphinx", "cairosvg", "corner", "emcee (>=3.0.0)", "ipykernel", "jupyter-sphinx (>=0.2.4)", "matplotlib", "numdifftools", "numexpr", "pandas", "pycairo", "sphinx-gallery (>=0.10)", "sphinxcontrib-svg2pdfconverter", "sympy"] -test = ["coverage", "flaky", "pytest", "pytest-cov"] - -[[package]] -name = "locket" -version = "1.0.0" -description = "File-based locks for Python on Linux and Windows" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3"}, - {file = "locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632"}, -] - -[[package]] -name = "lxml" -version = "5.3.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "matplotlib" -version = "3.9.4" -description = "Python plotting package" -optional = false -python-versions = ">=3.9" -files = [ - {file = "matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50"}, - {file = "matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff"}, - {file = "matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26"}, - {file = "matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50"}, - {file = "matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5"}, - {file = "matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d"}, - {file = "matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c"}, - {file = "matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099"}, - {file = "matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249"}, - {file = "matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423"}, - {file = "matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e"}, - {file = "matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3"}, - {file = "matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70"}, - {file = "matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483"}, - {file = "matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f"}, - {file = "matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00"}, - {file = "matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0"}, - {file = "matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b"}, - {file = "matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6"}, - {file = "matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45"}, - {file = "matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858"}, - {file = "matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64"}, - {file = "matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df"}, - {file = "matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799"}, - {file = "matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb"}, - {file = "matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a"}, - {file = "matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c"}, - {file = "matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764"}, - {file = "matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041"}, - {file = "matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965"}, - {file = "matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c"}, - {file = "matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7"}, - {file = "matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e"}, - {file = "matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c"}, - {file = "matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb"}, - {file = "matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db"}, - {file = "matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865"}, - {file = "matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.3.1" -numpy = ">=1.23" -packaging = ">=20.0" -pillow = ">=8" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "numpy (>=1.25)", "pybind11 (>=2.6,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mdit-py-plugins" -version = "0.4.2" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, - {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<4.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mistune" -version = "3.1.0" -description = "A sane and fast Markdown parser with useful plugins and renderers" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1"}, - {file = "mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667"}, -] - -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.11\""} - -[[package]] -name = "mypy" -version = "1.9.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "myst-parser" -version = "3.0.1" -description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," -optional = false -python-versions = ">=3.8" -files = [ - {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, - {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, -] - -[package.dependencies] -docutils = ">=0.18,<0.22" -jinja2 = "*" -markdown-it-py = ">=3.0,<4.0" -mdit-py-plugins = ">=0.4,<1.0" -pyyaml = "*" -sphinx = ">=6,<8" - -[package.extras] -code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] - -[[package]] -name = "natsort" -version = "8.4.0" -description = "Simple yet flexible natural sorting in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, - {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, -] - -[package.extras] -fast = ["fastnumbers (>=2.0.0)"] -icu = ["PyICU (>=1.0.0)"] - -[[package]] -name = "nbclient" -version = "0.10.2" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -optional = false -python-versions = ">=3.9.0" -files = [ - {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, - {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, -] - -[package.dependencies] -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" -traitlets = ">=5.4" - -[package.extras] -dev = ["pre-commit"] -docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.16.4" -description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "!=5.0.0" -defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<4" -nbclient = ">=0.5.0" -nbformat = ">=5.7" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.1" - -[package.extras] -all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["pyqtwebengine (>=5.15)"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] -webpdf = ["playwright"] - -[[package]] -name = "nbformat" -version = "5.10.4" -description = "The Jupyter Notebook format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, -] - -[package.dependencies] -fastjsonschema = ">=2.15" -jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -traitlets = ">=5.1" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "nbsphinx" -version = "0.9.6" -description = "Jupyter Notebook Tools for Sphinx" -optional = false -python-versions = ">=3.6" -files = [ - {file = "nbsphinx-0.9.6-py3-none-any.whl", hash = "sha256:336b0b557945a7678ec7449b16449f854bc852a435bb53b8a72e6b5dc740d992"}, - {file = "nbsphinx-0.9.6.tar.gz", hash = "sha256:c2b28a2d702f1159a95b843831798e86e60a17fc647b9bff9ba1585355de54e3"}, -] - -[package.dependencies] -docutils = ">=0.18.1" -jinja2 = "*" -nbconvert = ">=5.3,<5.4 || >5.4" -nbformat = "*" -sphinx = ">=1.8" -traitlets = ">=5" - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = true -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "networkx" -version = "3.2.1" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.9" -files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, -] - -[package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "notebook" -version = "7.3.2" -description = "Jupyter Notebook - A web-based notebook environment for interactive computing" -optional = true -python-versions = ">=3.8" -files = [ - {file = "notebook-7.3.2-py3-none-any.whl", hash = "sha256:e5f85fc59b69d3618d73cf27544418193ff8e8058d5bf61d315ce4f473556288"}, - {file = "notebook-7.3.2.tar.gz", hash = "sha256:705e83a1785f45b383bf3ee13cb76680b92d24f56fb0c7d2136fe1d850cd3ca8"}, -] - -[package.dependencies] -jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.3.4,<4.4" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2,<0.3" -tornado = ">=6.2.0" - -[package.extras] -dev = ["hatch", "pre-commit"] -docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] - -[[package]] -name = "notebook-shim" -version = "0.2.4" -description = "A shim layer for notebook traits and config" -optional = true -python-versions = ">=3.7" -files = [ - {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, - {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, -] - -[package.dependencies] -jupyter-server = ">=1.8,<3" - -[package.extras] -test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] - -[[package]] -name = "numba" -version = "0.60.0" -description = "compiling Python code using LLVM" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"}, - {file = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"}, - {file = "numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781"}, - {file = "numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e"}, - {file = "numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198"}, - {file = "numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8"}, - {file = "numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b"}, - {file = "numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703"}, - {file = "numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8"}, - {file = "numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2"}, - {file = "numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404"}, - {file = "numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c"}, - {file = "numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e"}, - {file = "numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d"}, - {file = "numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347"}, - {file = "numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74"}, - {file = "numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449"}, - {file = "numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b"}, - {file = "numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25"}, - {file = "numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab"}, - {file = "numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16"}, -] - -[package.dependencies] -llvmlite = "==0.43.*" -numpy = ">=1.22,<2.1" - -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, -] - -[[package]] -name = "opencv-python" -version = "4.10.0.84" -description = "Wrapper package for OpenCV python bindings." -optional = false -python-versions = ">=3.6" -files = [ - {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] - -[[package]] -name = "orjson" -version = "3.10.13" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "orjson-3.10.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1232c5e873a4d1638ef957c5564b4b0d6f2a6ab9e207a9b3de9de05a09d1d920"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26a0eca3035619fa366cbaf49af704c7cb1d4a0e6c79eced9f6a3f2437964b6"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4b6acd7c9c829895e50d385a357d4b8c3fafc19c5989da2bae11783b0fd4977"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1884e53c6818686891cc6fc5a3a2540f2f35e8c76eac8dc3b40480fb59660b00"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a428afb5720f12892f64920acd2eeb4d996595bf168a26dd9190115dbf1130d"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba5b13b8739ce5b630c65cb1c85aedbd257bcc2b9c256b06ab2605209af75a2e"}, - {file = "orjson-3.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cab83e67f6aabda1b45882254b2598b48b80ecc112968fc6483fa6dae609e9f0"}, - {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62c3cc00c7e776c71c6b7b9c48c5d2701d4c04e7d1d7cdee3572998ee6dc57cc"}, - {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dc03db4922e75bbc870b03fc49734cefbd50fe975e0878327d200022210b82d8"}, - {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22f1c9a30b43d14a041a6ea190d9eca8a6b80c4beb0e8b67602c82d30d6eec3e"}, - {file = "orjson-3.10.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b42f56821c29e697c68d7d421410d7c1d8f064ae288b525af6a50cf99a4b1200"}, - {file = "orjson-3.10.13-cp310-cp310-win32.whl", hash = "sha256:0dbf3b97e52e093d7c3e93eb5eb5b31dc7535b33c2ad56872c83f0160f943487"}, - {file = "orjson-3.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:46c249b4e934453be4ff2e518cd1adcd90467da7391c7a79eaf2fbb79c51e8c7"}, - {file = "orjson-3.10.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a36c0d48d2f084c800763473020a12976996f1109e2fcb66cfea442fdf88047f"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0065896f85d9497990731dfd4a9991a45b0a524baec42ef0a63c34630ee26fd6"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92b4ec30d6025a9dcdfe0df77063cbce238c08d0404471ed7a79f309364a3d19"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a94542d12271c30044dadad1125ee060e7a2048b6c7034e432e116077e1d13d2"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3723e137772639af8adb68230f2aa4bcb27c48b3335b1b1e2d49328fed5e244c"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f00c7fb18843bad2ac42dc1ce6dd214a083c53f1e324a0fd1c8137c6436269b"}, - {file = "orjson-3.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e2759d3172300b2f892dee85500b22fca5ac49e0c42cfff101aaf9c12ac9617"}, - {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee948c6c01f6b337589c88f8e0bb11e78d32a15848b8b53d3f3b6fea48842c12"}, - {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa6fe68f0981fba0d4bf9cdc666d297a7cdba0f1b380dcd075a9a3dd5649a69e"}, - {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbcd7aad6bcff258f6896abfbc177d54d9b18149c4c561114f47ebfe74ae6bfd"}, - {file = "orjson-3.10.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2149e2fcd084c3fd584881c7f9d7f9e5ad1e2e006609d8b80649655e0d52cd02"}, - {file = "orjson-3.10.13-cp311-cp311-win32.whl", hash = "sha256:89367767ed27b33c25c026696507c76e3d01958406f51d3a2239fe9e91959df2"}, - {file = "orjson-3.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:dca1d20f1af0daff511f6e26a27354a424f0b5cf00e04280279316df0f604a6f"}, - {file = "orjson-3.10.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a3614b00621c77f3f6487792238f9ed1dd8a42f2ec0e6540ee34c2d4e6db813a"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c976bad3996aa027cd3aef78aa57873f3c959b6c38719de9724b71bdc7bd14b"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f74d878d1efb97a930b8a9f9898890067707d683eb5c7e20730030ecb3fb930"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ef84f7e9513fb13b3999c2a64b9ca9c8143f3da9722fbf9c9ce51ce0d8076e"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd2bcde107221bb9c2fa0c4aaba735a537225104173d7e19cf73f70b3126c993"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:064b9dbb0217fd64a8d016a8929f2fae6f3312d55ab3036b00b1d17399ab2f3e"}, - {file = "orjson-3.10.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0044b0b8c85a565e7c3ce0a72acc5d35cda60793edf871ed94711e712cb637d"}, - {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7184f608ad563032e398f311910bc536e62b9fbdca2041be889afcbc39500de8"}, - {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d36f689e7e1b9b6fb39dbdebc16a6f07cbe994d3644fb1c22953020fc575935f"}, - {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54433e421618cd5873e51c0e9d0b9fb35f7bf76eb31c8eab20b3595bb713cd3d"}, - {file = "orjson-3.10.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1ba0c5857dd743438acecc1cd0e1adf83f0a81fee558e32b2b36f89e40cee8b"}, - {file = "orjson-3.10.13-cp312-cp312-win32.whl", hash = "sha256:a42b9fe4b0114b51eb5cdf9887d8c94447bc59df6dbb9c5884434eab947888d8"}, - {file = "orjson-3.10.13-cp312-cp312-win_amd64.whl", hash = "sha256:3a7df63076435f39ec024bdfeb4c9767ebe7b49abc4949068d61cf4857fa6d6c"}, - {file = "orjson-3.10.13-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2cdaf8b028a976ebab837a2c27b82810f7fc76ed9fb243755ba650cc83d07730"}, - {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a946796e390cbb803e069472de37f192b7a80f4ac82e16d6eb9909d9e39d56"}, - {file = "orjson-3.10.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d64f1db5ecbc21eb83097e5236d6ab7e86092c1cd4c216c02533332951afc"}, - {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:711878da48f89df194edd2ba603ad42e7afed74abcd2bac164685e7ec15f96de"}, - {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cf16f06cb77ce8baf844bc222dbcb03838f61d0abda2c3341400c2b7604e436e"}, - {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8257c3fb8dd7b0b446b5e87bf85a28e4071ac50f8c04b6ce2d38cb4abd7dff57"}, - {file = "orjson-3.10.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9c3a87abe6f849a4a7ac8a8a1dede6320a4303d5304006b90da7a3cd2b70d2c"}, - {file = "orjson-3.10.13-cp313-cp313-win32.whl", hash = "sha256:527afb6ddb0fa3fe02f5d9fba4920d9d95da58917826a9be93e0242da8abe94a"}, - {file = "orjson-3.10.13-cp313-cp313-win_amd64.whl", hash = "sha256:b5f7c298d4b935b222f52d6c7f2ba5eafb59d690d9a3840b7b5c5cda97f6ec5c"}, - {file = "orjson-3.10.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e49333d1038bc03a25fdfe11c86360df9b890354bfe04215f1f54d030f33c342"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:003721c72930dbb973f25c5d8e68d0f023d6ed138b14830cc94e57c6805a2eab"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63664bf12addb318dc8f032160e0f5dc17eb8471c93601e8f5e0d07f95003784"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6066729cf9552d70de297b56556d14b4f49c8f638803ee3c90fd212fa43cc6af"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a1152e2761025c5d13b5e1908d4b1c57f3797ba662e485ae6f26e4e0c466388"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b21d91c5c5ef8a201036d207b1adf3aa596b930b6ca3c71484dd11386cf6c3"}, - {file = "orjson-3.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b12a63f48bb53dba8453d36ca2661f2330126d54e26c1661e550b32864b28ce3"}, - {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a5a7624ab4d121c7e035708c8dd1f99c15ff155b69a1c0affc4d9d8b551281ba"}, - {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0fee076134398d4e6cb827002468679ad402b22269510cf228301b787fdff5ae"}, - {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ae537fcf330b3947e82c6ae4271e092e6cf16b9bc2cef68b14ffd0df1fa8832a"}, - {file = "orjson-3.10.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f81b26c03f5fb5f0d0ee48d83cea4d7bc5e67e420d209cc1a990f5d1c62f9be0"}, - {file = "orjson-3.10.13-cp38-cp38-win32.whl", hash = "sha256:0bc858086088b39dc622bc8219e73d3f246fb2bce70a6104abd04b3a080a66a8"}, - {file = "orjson-3.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:3ca6f17467ebbd763f8862f1d89384a5051b461bb0e41074f583a0ebd7120e8e"}, - {file = "orjson-3.10.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a11532cbfc2f5752c37e84863ef8435b68b0e6d459b329933294f65fa4bda1a"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c96d2fb80467d1d0dfc4d037b4e1c0f84f1fe6229aa7fea3f070083acef7f3d7"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dda4ba4d3e6f6c53b6b9c35266788053b61656a716a7fef5c884629c2a52e7aa"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f998bbf300690be881772ee9c5281eb9c0044e295bcd4722504f5b5c6092ff"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1cc42ed75b585c0c4dc5eb53a90a34ccb493c09a10750d1a1f9b9eff2bd12"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03b0f29d485411e3c13d79604b740b14e4e5fb58811743f6f4f9693ee6480a8f"}, - {file = "orjson-3.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:233aae4474078d82f425134bb6a10fb2b3fc5a1a1b3420c6463ddd1b6a97eda8"}, - {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e384e330a67cf52b3597ee2646de63407da6f8fc9e9beec3eaaaef5514c7a1c9"}, - {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4222881d0aab76224d7b003a8e5fdae4082e32c86768e0e8652de8afd6c4e2c1"}, - {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e400436950ba42110a20c50c80dff4946c8e3ec09abc1c9cf5473467e83fd1c5"}, - {file = "orjson-3.10.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f47c9e7d224b86ffb086059cdcf634f4b3f32480f9838864aa09022fe2617ce2"}, - {file = "orjson-3.10.13-cp39-cp39-win32.whl", hash = "sha256:a9ecea472f3eb653e1c0a3d68085f031f18fc501ea392b98dcca3e87c24f9ebe"}, - {file = "orjson-3.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:5385935a73adce85cc7faac9d396683fd813566d3857fa95a0b521ef84a5b588"}, - {file = "orjson-3.10.13.tar.gz", hash = "sha256:eb9bfb14ab8f68d9d9492d4817ae497788a15fd7da72e14dfabc289c3bb088ec"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pandas" -version = "2.2.3" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, - {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, - {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, - {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, - {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, - {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, - {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, - {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, - {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, - {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, - {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -description = "Utilities for writing pandoc filters in python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, - {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "partd" -version = "1.4.2" -description = "Appendable key-value storage" -optional = false -python-versions = ">=3.9" -files = [ - {file = "partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f"}, - {file = "partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c"}, -] - -[package.dependencies] -locket = "*" -toolz = "*" - -[package.extras] -complete = ["blosc", "numpy (>=1.20.0)", "pandas (>=1.3)", "pyzmq"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "photutils" -version = "1.11.0" -description = "An Astropy package for source detection and photometry" -optional = false -python-versions = ">=3.9" -files = [ - {file = "photutils-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d4f089f65fe9aeb3095ccf40e4784ea47a1a26e48888f1e11b1b87191b13d90"}, - {file = "photutils-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5d907ecc5511aa56108c2c8deece625628effcb8224676f4874b03d5e673bbee"}, - {file = "photutils-1.11.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fa60fc13976e8d4983ee8a8f071ec6703ef492a2761bff2b37e5e0549c5881a"}, - {file = "photutils-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c1fe7906f3bfe6d870efdf659b4f7e8c34aca959122805f0102a735df948002"}, - {file = "photutils-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a59bc830fbe083651b2a0709a6b2069bb8ef70a9e6025c9cf64cfe47b2abfa11"}, - {file = "photutils-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6419525f3c2a9dbe7c71a6e1e2e3dfec17ccb2a8cb3ed6f93b52dec25a6d5fe8"}, - {file = "photutils-1.11.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4f93bfdb12b32cad475f2bee4cb3e2187ac13f83ce0b06f12f17569f4da2b9"}, - {file = "photutils-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:3cd5bb00eab472193d467f6d0385842117948e7f842d9e254563ff06935ccdad"}, - {file = "photutils-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3699d65f1e9697da84a413a4cfbd00a91730b44ef79664ae0423016fdf38ece0"}, - {file = "photutils-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43fa6b850a42700621f1083b639cb7c1e55abccf16db76274dc33ba1a877c64d"}, - {file = "photutils-1.11.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a130a515794156897e74859e255dd3f880c12233495a1cd34295295da2c34e35"}, - {file = "photutils-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:2514f2a6a1da90f30e0a3a2cd538bfcc281dbddb60e6ec5a3b665e761d8835d9"}, - {file = "photutils-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53b251ebd5ffab83035d04f79bc65e18c64479b36d1fe0ef8817a4606b3c595b"}, - {file = "photutils-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e18b24319bd16f096b1dfd821b3e5e672bce7dbbf18bc32c2e51c53f7b43660b"}, - {file = "photutils-1.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8530421f849a0aa158aed02aa3cd7cd8c18843402eefd38ebd34565f7b985e18"}, - {file = "photutils-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:f2680ea011d5955384c9b1b5e2730018bd45ebaafc462ed2893666ba6313f886"}, - {file = "photutils-1.11.0.tar.gz", hash = "sha256:ee709d090bac2bc8b8b317078c87f9c4b6855f6f94c2f7f2a93ab5b8f8375597"}, -] - -[package.dependencies] -astropy = ">=5.1" -numpy = ">=1.22" - -[package.extras] -all = ["bottleneck", "gwcs (>=0.18)", "matplotlib (>=3.5)", "rasterio", "scikit-image (>=0.19)", "scikit-learn (>=1.0)", "scipy (>=1.7.2)", "shapely", "tqdm"] -docs = ["photutils[all]", "sphinx", "sphinx-astropy (>=1.6)", "tomli"] -test = ["pytest-astropy (>=0.10)"] - -[[package]] -name = "pillow" -version = "11.0.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, - {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, - {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, - {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, - {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, - {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, - {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, - {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, - {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, - {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, - {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, - {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, - {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, - {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, - {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, - {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, - {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, - {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, - {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, - {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, - {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, - {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pprintpp" -version = "0.4.0" -description = "A drop-in replacement for pprint that's actually pretty" -optional = false -python-versions = "*" -files = [ - {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, - {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, -] - -[[package]] -name = "pre-commit" -version = "4.0.1" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, - {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "prometheus-client" -version = "0.21.1" -description = "Python client for the Prometheus monitoring system." -optional = false -python-versions = ">=3.8" -files = [ - {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, - {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, -] - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.48" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, - {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "psutil" -version = "6.1.1" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, - {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, - {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, - {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, - {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, - {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, - {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, - {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, - {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, - {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, - {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, - {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, -] - -[package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pyarrow" -version = "16.1.0" -description = "Python library for Apache Arrow" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"}, - {file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"}, - {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"}, - {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"}, - {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"}, - {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"}, - {file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"}, - {file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"}, - {file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"}, - {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"}, - {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"}, - {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"}, - {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"}, - {file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"}, - {file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"}, - {file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"}, - {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"}, - {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"}, - {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"}, - {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"}, - {file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"}, - {file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"}, - {file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"}, - {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"}, - {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"}, - {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"}, - {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"}, - {file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"}, - {file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"}, - {file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"}, - {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"}, - {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"}, - {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"}, - {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"}, - {file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"}, - {file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"}, -] - -[package.dependencies] -numpy = ">=1.16.6" - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.10.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydata-sphinx-theme" -version = "0.15.4" -description = "Bootstrap-based Sphinx theme from the PyData community" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6"}, - {file = "pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d"}, -] - -[package.dependencies] -accessible-pygments = "*" -Babel = "*" -beautifulsoup4 = "*" -docutils = "!=0.17.0" -packaging = "*" -pygments = ">=2.7" -sphinx = ">=5" -typing-extensions = "*" - -[package.extras] -a11y = ["pytest-playwright"] -dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] -doc = ["ablog (>=0.11.8)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (>=3.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] -i18n = ["Babel", "jinja2"] -test = ["pytest", "pytest-cov", "pytest-regressions", "sphinx[test]"] - -[[package]] -name = "pyerfa" -version = "2.0.1.5" -description = "Python bindings for ERFA" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b282d7c60c4c47cf629c484c17ac504fcb04abd7b3f4dfcf53ee042afc3a5944"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:be1aeb70390dd03a34faf96749d5cabc58437410b4aab7213c512323932427df"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0603e8e1b839327d586c8a627cdc634b795e18b007d84f0cda5500a0908254e"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e43c7194e3242083f2350b46c09fd4bf8ba1bcc0ebd1460b98fc47fe2389906"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:07b80cd70701f5d066b1ac8cce406682cfcd667a1186ec7d7ade597239a6021d"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-win32.whl", hash = "sha256:d30b9b0df588ed5467e529d851ea324a67239096dd44703125072fd11b351ea2"}, - {file = "pyerfa-2.0.1.5-cp39-abi3-win_amd64.whl", hash = "sha256:66292d437dcf75925b694977aa06eb697126e7b86553e620371ed3e48b5e0ad0"}, - {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4991dee680ff36c87911d8faa4c7d1aa6278ad9b5e0d16158cf22fa7d74ba25c"}, - {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690e258294202c86f479e78e80fd235cd27bd717f7f60062fccc3dbd6ef0b1a9"}, - {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:171ce9676a448a7eb555f03aa19ad5c749dbced1ce4f9923e4d93443c4a9c612"}, - {file = "pyerfa-2.0.1.5.tar.gz", hash = "sha256:17d6b24fe4846c65d5e7d8c362dcb08199dc63b30a236aedd73875cc83e1f6c0"}, -] - -[package.dependencies] -numpy = ">=1.19.3" - -[package.extras] -docs = ["sphinx-astropy (>=1.3)"] -test = ["pytest", "pytest-doctestplus (>=0.7)"] - -[[package]] -name = "pyfakefs" -version = "5.7.3" -description = "pyfakefs implements a fake file system that mocks the Python file system modules." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyfakefs-5.7.3-py3-none-any.whl", hash = "sha256:53702780b38b24a48a9b8481c971abf1675f5abfd7d44653c2bcdd90b9751224"}, - {file = "pyfakefs-5.7.3.tar.gz", hash = "sha256:cd53790761d0fc030a9cf41fd541bfd28c1ea681b1a7c5df8834f3c9e511ac5f"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pynxtools" -version = "0.9.3" -description = "Extend NeXus for experiments and characterization in Materials Science and Materials Engineering and serve as a NOMAD parser implementation for NeXus." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pynxtools-0.9.3-py3-none-any.whl", hash = "sha256:8b4a783bac5bdbfa284ac0b4a491fe7cb2b529616388bede11c6024e8e84a53c"}, - {file = "pynxtools-0.9.3.tar.gz", hash = "sha256:ea3374e7183d014745583ed4b4605d3039345fc628b323303b106cf8d3820ddd"}, -] - -[package.dependencies] -anytree = "*" -ase = ">=3.19.0" -click = ">=7.1.2" -click_default_group = "*" -h5py = ">=3.6.0" -importlib-metadata = "*" -lxml = ">=4.9.1" -mergedeep = "*" -numpy = ">=1.22.4,<2.0.0" -pandas = ">=1.3.2" -PyYAML = ">=6.0" -xarray = ">=0.20.2" - -[package.extras] -apm = ["pynxtools-apm (>=0.2.2)"] -convert = ["pynxtools[apm,ellips,em,igor,mpes,raman,stm,xps,xrd]"] -dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-timeout", "ruff (>=0.6)", "structlog", "types-pytz", "types-pyyaml", "types-requests", "uv"] -docs = ["markdown-include", "mkdocs", "mkdocs-click", "mkdocs-macros-plugin", "mkdocs-material", "mkdocs-material-extensions"] -ellips = ["pynxtools-ellips (>=0.0.7)"] -em = ["pynxtools-em (>=0.3.1)"] -igor = ["pynxtools-igor (>=0.1.0)"] -mpes = ["pynxtools-mpes (>=0.2.1)"] -raman = ["pynxtools-raman (>=0.0.5)"] -stm = ["pynxtools-spm (>=0.0.0)"] -xps = ["pynxtools-xps (>=0.4.8)"] -xrd = ["pynxtools-xrd (>=0.0.3)"] - -[[package]] -name = "pynxtools-mpes" -version = "0.2.1" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pynxtools_mpes-0.2.1-py3-none-any.whl", hash = "sha256:a6df87f11787ff67af896ca1310f409597882be8c09fa3124a3a789a3071f69e"}, - {file = "pynxtools_mpes-0.2.1.tar.gz", hash = "sha256:8f3be7f0ff0f35b51f8e436b87893c14b994bf983ccff04b95b6a6c6a8271e97"}, -] - -[package.dependencies] -h5py = ">=3.6.0" -pynxtools = ">=0.6.0" -xarray = ">=0.20.2" - -[package.extras] -dev = ["mypy", "pip-tools", "pre-commit", "pytest", "ruff (>=0.6.0)", "types-pyyaml"] -docs = ["mkdocs", "mkdocs-click", "mkdocs-macros-plugin", "mkdocs-material", "mkdocs-material-extensions", "pymdown-extensions"] - -[[package]] -name = "pyparsing" -version = "3.2.0" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, - {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-clarity" -version = "1.0.1" -description = "A plugin providing an alternative, colourful diff output for failing assertions." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"}, -] - -[package.dependencies] -pprintpp = ">=0.4.0" -pytest = ">=3.5.0" -rich = ">=8.0.0" - -[[package]] -name = "pytest-cov" -version = "6.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-json-logger" -version = "3.2.1" -description = "JSON Log Formatter for the Python Logging Package" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090"}, - {file = "python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008"}, -] - -[package.dependencies] -typing_extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "msgspec-python313-pre", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] - -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - -[[package]] -name = "pywin32" -version = "308" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -files = [ - {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, - {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, - {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, - {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, - {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, - {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, - {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, - {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, - {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, - {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, - {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, - {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, - {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, - {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, - {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, - {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, - {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, - {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, -] - -[[package]] -name = "pywinpty" -version = "2.0.14" -description = "Pseudo terminal support for Windows from Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f"}, - {file = "pywinpty-2.0.14-cp311-none-win_amd64.whl", hash = "sha256:cf2a43ac7065b3e0dc8510f8c1f13a75fb8fde805efa3b8cff7599a1ef497bc7"}, - {file = "pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737"}, - {file = "pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819"}, - {file = "pywinpty-2.0.14-cp39-none-win_amd64.whl", hash = "sha256:5725fd56f73c0531ec218663bd8c8ff5acc43c78962fab28564871b5fce053fd"}, - {file = "pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyzmq" -version = "26.2.0" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, - {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, - {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, - {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, - {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, - {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, - {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, - {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, - {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, - {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, - {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, - {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, - {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, - {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, - {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, - {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, - {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, - {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, - {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, - {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, - {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, - {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, - {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, - {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, - {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, - {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, - {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, - {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, - {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, - {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, - {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, - {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, - {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, - {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, - {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, - {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, - {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, - {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, - {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, - {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, - {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, - {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, - {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, - {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, - {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, - {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, - {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, - {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, - {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, - {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, - {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, - {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, - {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, - {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, - {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "recommonmark" -version = "0.7.1" -description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." -optional = false -python-versions = "*" -files = [ - {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, - {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, -] - -[package.dependencies] -commonmark = ">=0.8.1" -docutils = ">=0.11" -sphinx = ">=1.3.1" - -[[package]] -name = "referencing" -version = "0.35.1" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-mock" -version = "1.12.1" -description = "Mock out responses from the requests package" -optional = false -python-versions = ">=3.5" -files = [ - {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, - {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, -] - -[package.dependencies] -requests = ">=2.22,<3" - -[package.extras] -fixture = ["fixtures"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -description = "Pure python rfc3986 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, - {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, -] - -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.22.3" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, - {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, - {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, - {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, - {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, - {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, - {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, - {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, - {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, - {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, - {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, - {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, - {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, - {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, - {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, - {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, - {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, - {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, - {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, - {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, - {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, - {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, - {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, - {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, - {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, - {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, - {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, - {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, - {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, - {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, - {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, - {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, - {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, - {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, - {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, - {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, - {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, - {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, - {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, - {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, - {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, - {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, - {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, - {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, - {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, - {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, - {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, - {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, - {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, - {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, - {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, -] - -[[package]] -name = "ruff" -version = "0.2.2" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, -] - -[[package]] -name = "scikit-image" -version = "0.24.0" -description = "Image processing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "scikit_image-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3bc0264b6ab30b43c4179ee6156bc18b4861e78bb329dd8d16537b7bbf827a"}, - {file = "scikit_image-0.24.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9c7a52e20cdd760738da38564ba1fed7942b623c0317489af1a598a8dedf088b"}, - {file = "scikit_image-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93f46e6ce42e5409f4d09ce1b0c7f80dd7e4373bcec635b6348b63e3c886eac8"}, - {file = "scikit_image-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ee0af13435c57351a3397eb379e72164ff85161923eec0c38849fecf1b4764"}, - {file = "scikit_image-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ac7913b028b8aa780ffae85922894a69e33d1c0bf270ea1774f382fe8bf95e7"}, - {file = "scikit_image-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:272909e02a59cea3ed4aa03739bb88df2625daa809f633f40b5053cf09241831"}, - {file = "scikit_image-0.24.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:190ebde80b4470fe8838764b9b15f232a964f1a20391663e31008d76f0c696f7"}, - {file = "scikit_image-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c98cc695005faf2b79904e4663796c977af22586ddf1b12d6af2fa22842dc2"}, - {file = "scikit_image-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa27b3a0dbad807b966b8db2d78da734cb812ca4787f7fbb143764800ce2fa9c"}, - {file = "scikit_image-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:dacf591ac0c272a111181afad4b788a27fe70d213cfddd631d151cbc34f8ca2c"}, - {file = "scikit_image-0.24.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6fccceb54c9574590abcddc8caf6cefa57c13b5b8b4260ab3ff88ad8f3c252b3"}, - {file = "scikit_image-0.24.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ccc01e4760d655aab7601c1ba7aa4ddd8b46f494ac46ec9c268df6f33ccddf4c"}, - {file = "scikit_image-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18836a18d3a7b6aca5376a2d805f0045826bc6c9fc85331659c33b4813e0b563"}, - {file = "scikit_image-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8579bda9c3f78cb3b3ed8b9425213c53a25fa7e994b7ac01f2440b395babf660"}, - {file = "scikit_image-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:82ab903afa60b2da1da2e6f0c8c65e7c8868c60a869464c41971da929b3e82bc"}, - {file = "scikit_image-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef04360eda372ee5cd60aebe9be91258639c86ae2ea24093fb9182118008d009"}, - {file = "scikit_image-0.24.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e9aadb442360a7e76f0c5c9d105f79a83d6df0e01e431bd1d5757e2c5871a1f3"}, - {file = "scikit_image-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e37de6f4c1abcf794e13c258dc9b7d385d5be868441de11c180363824192ff7"}, - {file = "scikit_image-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4688c18bd7ec33c08d7bf0fd19549be246d90d5f2c1d795a89986629af0a1e83"}, - {file = "scikit_image-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:56dab751d20b25d5d3985e95c9b4e975f55573554bd76b0aedf5875217c93e69"}, - {file = "scikit_image-0.24.0.tar.gz", hash = "sha256:5d16efe95da8edbeb363e0c4157b99becbd650a60b77f6e3af5768b66cf007ab"}, -] - -[package.dependencies] -imageio = ">=2.33" -lazy-loader = ">=0.4" -networkx = ">=2.8" -numpy = ">=1.23" -packaging = ">=21" -pillow = ">=9.1" -scipy = ">=1.9" -tifffile = ">=2022.8.12" - -[package.extras] -build = ["Cython (>=3.0.4)", "build", "meson-python (>=0.15)", "ninja", "numpy (>=2.0.0rc1)", "packaging (>=21)", "pythran", "setuptools (>=67)", "spin (==0.8)", "wheel"] -data = ["pooch (>=1.6.0)"] -developer = ["ipython", "pre-commit", "tomli"] -docs = ["PyWavelets (>=1.1.1)", "dask[array] (>=2022.9.2)", "ipykernel", "ipywidgets", "kaleido", "matplotlib (>=3.6)", "myst-parser", "numpydoc (>=1.7)", "pandas (>=1.5)", "plotly (>=5.10)", "pooch (>=1.6)", "pydata-sphinx-theme (>=0.15.2)", "pytest-doctestplus", "pytest-runner", "scikit-learn (>=1.1)", "seaborn (>=0.11)", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-gallery (>=0.14)", "sphinx_design (>=0.5)", "tifffile (>=2022.8.12)"] -optional = ["PyWavelets (>=1.1.1)", "SimpleITK", "astropy (>=5.0)", "cloudpickle (>=0.2.1)", "dask[array] (>=2021.1.0)", "matplotlib (>=3.6)", "pooch (>=1.6.0)", "pyamg", "scikit-learn (>=1.1)"] -test = ["asv", "numpydoc (>=1.7)", "pooch (>=1.6.0)", "pytest (>=7.0)", "pytest-cov (>=2.11.0)", "pytest-doctestplus", "pytest-faulthandler", "pytest-localserver"] - -[[package]] -name = "scipy" -version = "1.13.1" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, -] - -[package.dependencies] -numpy = ">=1.22.4,<2.3" - -[package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "send2trash" -version = "1.8.3" -description = "Send file to trash natively under Mac OS X, Windows and Linux" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, - {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, -] - -[package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] - -[[package]] -name = "setuptools" -version = "75.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = true -python-versions = ">=3.9" -files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.6" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, -] - -[[package]] -name = "sphinx" -version = "7.4.7" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, - {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, -] - -[package.dependencies] -alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "2.3.0" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, - {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, -] - -[package.dependencies] -sphinx = ">=7.3.5" - -[package.extras] -docs = ["furo (>=2024.1.29)"] -numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] - -[[package]] -name = "sphinx-rtd-theme" -version = "3.0.2" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, - {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, -] - -[package.dependencies] -docutils = ">0.18,<0.22" -sphinx = ">=6,<9" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "transifex-client", "twine", "wheel"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "symmetrize" -version = "0.5.5" -description = "Symmetrization and centering of 2D pattern using nonrigid point set registration" -optional = false -python-versions = "*" -files = [ - {file = "symmetrize-0.5.5-py2.py3-none-any.whl", hash = "sha256:42c26ebcfbe6124fcda79359042a83a174217166f6bdbd4dfab807b9b9a218c1"}, - {file = "symmetrize-0.5.5.tar.gz", hash = "sha256:a88518d1cf825eb60cbcb174e3c9946000205a149d98954120a06e74d991961d"}, -] - -[package.dependencies] -astropy = "*" -matplotlib = "*" -numpy = "*" -opencv-python = "*" -photutils = "*" -recommonmark = "*" -scikit-image = "*" -scipy = "*" -sphinx = "*" -sphinx-rtd-theme = "*" - -[[package]] -name = "terminado" -version = "0.18.1" -description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, - {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, -] - -[package.dependencies] -ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} -tornado = ">=6.1.0" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] -typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] - -[[package]] -name = "threadpoolctl" -version = "3.5.0" -description = "threadpoolctl" -optional = false -python-versions = ">=3.8" -files = [ - {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, - {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, -] - -[[package]] -name = "tifffile" -version = "2024.8.30" -description = "Read and write TIFF files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "tifffile-2024.8.30-py3-none-any.whl", hash = "sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad"}, - {file = "tifffile-2024.8.30.tar.gz", hash = "sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4"}, -] - -[package.dependencies] -numpy = "*" - -[package.extras] -all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib", "zarr"] -codecs = ["imagecodecs (>=2023.8.12)"] -plot = ["matplotlib"] -test = ["cmapfile", "czifile", "dask", "defusedxml", "fsspec", "imagecodecs", "lfdfiles", "lxml", "ndtiff", "oiffile", "psdtags", "pytest", "roifile", "xarray", "zarr"] -xml = ["defusedxml", "lxml"] -zarr = ["fsspec", "zarr"] - -[[package]] -name = "tinycss2" -version = "1.4.0" -description = "A tiny CSS parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, - {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["pytest", "ruff"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - -[[package]] -name = "toolz" -version = "1.0.0" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, - {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, -] - -[[package]] -name = "tornado" -version = "6.4.2" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">=3.8" -files = [ - {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, - {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, - {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, - {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, - {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20241230" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, - {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, -] - -[[package]] -name = "types-requests" -version = "2.32.0.20241016" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - -[[package]] -name = "uncertainties" -version = "3.2.2" -description = "calculations with values with uncertainties, error propagation" -optional = false -python-versions = ">=3.8" -files = [ - {file = "uncertainties-3.2.2-py3-none-any.whl", hash = "sha256:fd8543355952f4052786ed4150acaf12e23117bd0f5bd03ea07de466bce646e7"}, - {file = "uncertainties-3.2.2.tar.gz", hash = "sha256:e62c86fdc64429828229de6ab4e11466f114907e6bd343c077858994cc12e00b"}, -] - -[package.extras] -all = ["uncertainties[arrays,doc,test]"] -arrays = ["numpy"] -doc = ["python-docs-theme", "sphinx", "sphinx-copybutton"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "uri-template" -version = "1.3.0" -description = "RFC 6570 URI Template Processor" -optional = false -python-versions = ">=3.7" -files = [ - {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, - {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, -] - -[package.extras] -dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] - -[[package]] -name = "urllib3" -version = "2.3.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.28.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -files = [ - {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, - {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "webcolors" -version = "24.11.1" -description = "A library for working with the color formats defined by HTML and CSS." -optional = false -python-versions = ">=3.9" -files = [ - {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, - {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "widgetsnbextension" -version = "4.0.13" -description = "Jupyter interactive widgets for Jupyter Notebook" -optional = false -python-versions = ">=3.7" -files = [ - {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, - {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, -] - -[[package]] -name = "xarray" -version = "2024.7.0" -description = "N-D labeled arrays and datasets in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "xarray-2024.7.0-py3-none-any.whl", hash = "sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64"}, - {file = "xarray-2024.7.0.tar.gz", hash = "sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638"}, -] - -[package.dependencies] -numpy = ">=1.23" -packaging = ">=23.1" -pandas = ">=2.0" - -[package.extras] -accel = ["bottleneck", "flox", "numbagg", "opt-einsum", "scipy"] -complete = ["xarray[accel,dev,io,parallel,viz]"] -dev = ["hypothesis", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "xarray[complete]"] -io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] -parallel = ["dask[complete]"] -viz = ["matplotlib", "nc-time-axis", "seaborn"] - -[[package]] -name = "xyzservices" -version = "2024.9.0" -description = "Source of XYZ tiles providers" -optional = false -python-versions = ">=3.8" -files = [ - {file = "xyzservices-2024.9.0-py3-none-any.whl", hash = "sha256:776ae82b78d6e5ca63dd6a94abb054df8130887a4a308473b54a6bd364de8644"}, - {file = "xyzservices-2024.9.0.tar.gz", hash = "sha256:68fb8353c9dbba4f1ff6c0f2e5e4e596bb9e1db7f94f4f7dfbcb26e25aa66fde"}, -] - -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[extras] -all = [] -notebook = ["ipykernel", "jupyter", "jupyterlab", "jupyterlab-h5web"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.9, <3.13" -content-hash = "6fe6a89d3ce81743b10770e2db102cc23605a889cec67f22763c5f8e7f603995" diff --git a/pyproject.toml b/pyproject.toml index 15a3d662..20b17cf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,78 +1,85 @@ -[tool.poetry] +[build-system] +requires = ["setuptools>=64.0.1", "setuptools-scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = [ + "src", +] + +[tool.setuptools_scm] + +[project] name = "sed-processor" -packages = [{include = "sed", from = "src"}] version = "1.0.0a0" description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" -authors = ["OpenCOMPES team "] +authors = [ + {name = "OpenCOMPES team", email = "sed-processor@mpes.science"}, +] readme = "README.md" -repository = "https://github.com/OpenCOMPES/sed" -documentation = "https://opencompes.github.io/sed/" keywords = ["sed", "mpes", "flash", "arpes"] -license = "MIT" - -[tool.poetry.dependencies] -python = ">=3.9, <3.13" -bokeh = ">=2.4.2" -dask = ">=2021.12.0, <2024.8" -fastdtw = ">=0.3.4" -h5py = ">=3.6.0" -ipympl = ">=0.9.1" -ipywidgets = "^8.1.5" -lmfit = ">=1.0.3" -matplotlib = ">=3.5.1" -natsort = ">=8.1.0" -numba = ">=0.55.1" -numpy = ">=1.18, <2.0" -pandas = ">=1.4.1" -psutil = ">=5.9.0" -pynxtools-mpes = ">=0.2.0" -pynxtools = ">=0.8.0" -pyyaml = ">=6.0.0" -scipy = ">=1.8.0" -symmetrize = ">=0.5.5" -threadpoolctl = ">=3.1.0" -tifffile = ">=2022.2.9" -tqdm = ">=4.62.3" -xarray = ">=0.20.2" -joblib = ">=1.2.0" -pyarrow = ">=14.0.1, <17.0" -pydantic = ">=2.8.2" -jupyter = {version = ">=1.0.0", optional = true} -ipykernel = {version = ">=6.9.1", optional = true} -jupyterlab = {version = ">=4.1.8", optional = true} -jupyterlab-h5web = {version = ">=8.0.0", extras = ["full"]} - -[tool.poetry.extras] -notebook = ["jupyter", "ipykernel", "jupyterlab", "jupyterlab-h5web"] -all = ["notebook"] - -[tool.poetry.group.dev.dependencies] -pytest = ">=7.0.1" -pytest-cov = ">=3.0.0" -pytest-xdist = ">=2.5.0" -pytest-clarity = ">=1.0.1" -ruff = ">=0.1.7, <0.3.0" -mypy = ">=1.6.0, <1.10.0" -types-pyyaml = ">=6.0.12.12" -types-requests = ">=2.31.0.9" -pyfakefs = ">=5.3.0" -requests-mock = "^1.11.0" -pre-commit = ">=3.0.0" - -[tool.poetry.group.docs] -optional = true +license = {text = "MIT"} +requires-python = ">=3.8,<3.13" +dependencies = [ + "bokeh>=2.4.2", + "dask>=2021.12.0,<2024.8", + "fastdtw>=0.3.4", + "h5py>=3.6.0", + "ipympl>=0.9.1", + "ipywidgets>=8.1.5", + "lmfit>=1.0.3", + "matplotlib>=3.5.1", + "natsort>=8.1.0", + "numba>=0.55.1", + "numpy>=1.18,<2.0", + "pandas>=1.4.1", + "psutil>=5.9.0", + "pynxtools-mpes>=0.2.0", + "pynxtools>=0.9.0", + "pyyaml>=6.0.0", + "scipy>=1.8.0", + "symmetrize>=0.5.5", + "threadpoolctl>=3.1.0", + "tifffile>=2022.2.9", + "tqdm>=4.62.3", + "xarray>=0.20.2", + "joblib>=1.2.0", + "pyarrow>=14.0.1,<17.0", + "pydantic>=2.8.2", +] -[tool.poetry.group.docs.dependencies] -sphinx = ">=7.1.2" -tomlkit = ">=0.12.0" -sphinx-autodoc-typehints = ">=1.17.0" -nbsphinx = ">=0.9.3" -myst-parser = ">=2.0.0" -pydata-sphinx-theme = "^0.15.0" +[project.urls] +repository = "https://github.com/OpenCOMPES/sed" +documentation = "https://opencompes.github.io/sed/" -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[project.optional-dependencies] +dev = [ + "pytest>=7.0.1", + "pytest-cov>=3.0.0", + "pytest-xdist>=2.5.0", + "pytest-clarity>=1.0.1", + "ruff<0.3.0,>=0.1.7", + "mypy<1.10.0,>=1.6.0", + "types-pyyaml>=6.0.12.12", + "types-requests>=2.31.0.9", + "pyfakefs>=5.3.0", + "requests-mock>=1.11.0", + "pre-commit>=3.0.0", +] +docs = [ + "sphinx>=7.1.2", + "tomlkit>=0.12.0", + "sphinx-autodoc-typehints>=1.17.0", + "nbsphinx>=0.9.3", + "myst-parser>=2.0.0", + "pydata-sphinx-theme>=0.15.0", +] +notebook = [ + "jupyter>=1.0.0", + "ipykernel>=6.9.1", + "jupyterlab>=4.0", + "jupyterlab-h5web>=8.0.0", +] [tool.coverage.report] omit = [ From 1118d5444742e079426c7ce6f5725a181c344c51 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 00:46:33 +0100 Subject: [PATCH 260/300] use dynamic versioning --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 20b17cf3..63104d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ where = [ [project] name = "sed-processor" -version = "1.0.0a0" +dynamic = ["version"] description = "Single Event Data Frame Processor: Backend to handle photoelectron resolved datastreams" authors = [ {name = "OpenCOMPES team", email = "sed-processor@mpes.science"}, From e07c99e41305f853d157af93835d041adad9422f Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 00:51:37 +0100 Subject: [PATCH 261/300] fix spelling --- cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 5826d5ab..585f1883 100644 --- a/cspell.json +++ b/cspell.json @@ -4,7 +4,8 @@ "./tests/data/*", "*.toml", "Makefile", - "*.bat" + "*.bat", + "*.egg-info", ], "dictionaryDefinitions": [ { From a50e9b81edcf4d6f3ab76ddfffbf304f2b19ee57 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 00:54:47 +0100 Subject: [PATCH 262/300] add v1 branch --- .github/workflows/testing_coverage.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index f8d2cc02..4fa12fd9 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -3,12 +3,11 @@ name: pytest and coverage report # Triggers the workflow on push for all branches and PR only for main on: push: - branches: [ main ] + branches: [ main, v1_feature_branch ] paths-ignore: pyproject.toml pull_request: - branches: - - main + branches: [ main, v1_feature_branch ] env: UV_SYSTEM_PYTHON: true From 4049c1ea6de171a103044d25215b753e4527e1a9 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 01:31:46 +0100 Subject: [PATCH 263/300] update tests --- tests/calibrator/test_delay.py | 5 ++--- tests/calibrator/test_energy.py | 7 +++---- tests/calibrator/test_momentum.py | 8 +++----- tests/loader/flash/conftest.py | 16 ++++++---------- tests/loader/mpes/test_mpes_loader.py | 6 ++---- tests/loader/sxp/test_sxp_loader.py | 5 ++--- tests/loader/test_loaders.py | 6 ++---- tests/loader/test_mirrorutil.py | 9 ++++----- tests/test_config.py | 12 +++++------- tests/test_diagnostics.py | 10 ++++------ tests/test_processor.py | 16 ++++++++-------- 11 files changed, 41 insertions(+), 59 deletions(-) diff --git a/tests/calibrator/test_delay.py b/tests/calibrator/test_delay.py index 5b6838ed..744b1373 100644 --- a/tests/calibrator/test_delay.py +++ b/tests/calibrator/test_delay.py @@ -3,7 +3,6 @@ from __future__ import annotations import os -from importlib.util import find_spec from typing import Any import dask.dataframe @@ -15,8 +14,8 @@ from sed.core.config import parse_config from sed.loader.loader_interface import get_loader -package_dir = os.path.dirname(find_spec("sed").origin) -file = package_dir + "/../../tests/data/loader/mpes/Scan0030_2.h5" +test_dir = os.path.join(os.path.dirname(__file__), "..") +file = test_dir + "/data/loader/mpes/Scan0030_2.h5" def test_delay_parameters_from_file() -> None: diff --git a/tests/calibrator/test_energy.py b/tests/calibrator/test_energy.py index fdbda343..81c877cf 100644 --- a/tests/calibrator/test_energy.py +++ b/tests/calibrator/test_energy.py @@ -7,7 +7,6 @@ import itertools import os from copy import deepcopy -from importlib.util import find_spec from typing import Any from typing import Literal @@ -21,9 +20,9 @@ from sed.core.config import parse_config from sed.loader.loader_interface import get_loader -package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../../tests/data/loader/mpes/" -folder = package_dir + "/../../tests/data/calibrator/" +test_dir = os.path.join(os.path.dirname(__file__), "..") +df_folder = test_dir + "/data/loader/mpes/" +folder = test_dir + "/data/calibrator/" files = glob.glob(df_folder + "*.h5") traces_list = [] diff --git a/tests/calibrator/test_momentum.py b/tests/calibrator/test_momentum.py index a8e2f166..dcddaa85 100644 --- a/tests/calibrator/test_momentum.py +++ b/tests/calibrator/test_momentum.py @@ -5,7 +5,6 @@ import csv import glob import os -from importlib.util import find_spec from typing import Any import numpy as np @@ -16,10 +15,9 @@ from sed.core.config import parse_config from sed.loader.loader_interface import get_loader -# pylint: disable=duplicate-code -package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../../tests/data/loader/mpes/" -folder = package_dir + "/../../tests/data/calibrator/" +test_dir = os.path.join(os.path.dirname(__file__), "..") +df_folder = test_dir + "/data/loader/mpes/" +folder = test_dir + "/data/calibrator/" files = glob.glob(df_folder + "*.h5") momentum_map_list = [] diff --git a/tests/loader/flash/conftest.py b/tests/loader/flash/conftest.py index 3cd98275..eac22c39 100644 --- a/tests/loader/flash/conftest.py +++ b/tests/loader/flash/conftest.py @@ -2,7 +2,6 @@ """ import os import shutil -from importlib.util import find_spec from pathlib import Path import h5py @@ -10,8 +9,8 @@ from sed.core.config import parse_config -package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../../tests/data/loader/flash/config.yaml") +test_dir = os.path.join(os.path.dirname(__file__), "../..") +config_path = os.path.join(test_dir, "data/loader/flash/config.yaml") H5_PATH = "FLASH1_USER3_stream_2_run43878_file1_20230130T153807.1.h5" H5_PATHS = [H5_PATH, "FLASH1_USER3_stream_2_run43879_file1_20230130T153807.1.h5"] @@ -45,7 +44,7 @@ def fixture_h5_file() -> h5py.File: Returns: h5py.File: The open h5 file. """ - return h5py.File(os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATH}"), "r") + return h5py.File(os.path.join(test_dir, f"data/loader/flash/{H5_PATH}"), "r") @pytest.fixture(name="h5_file_copy") @@ -56,7 +55,7 @@ def fixture_h5_file_copy(tmp_path: Path) -> h5py.File: h5py.File: The open h5 file copy. """ # Create a copy of the h5 file in a temporary directory - original_file_path = os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATH}") + original_file_path = os.path.join(test_dir, f"data/loader/flash/{H5_PATH}") copy_file_path = tmp_path / "copy.h5" shutil.copyfile(original_file_path, copy_file_path) @@ -72,7 +71,7 @@ def fixture_h5_file2_copy(tmp_path: Path) -> h5py.File: h5py.File: The open h5 file copy. """ # Create a copy of the h5 file in a temporary directory - original_file_path = os.path.join(package_dir, f"../../tests/data/loader/flash/{H5_PATHS[1]}") + original_file_path = os.path.join(test_dir, f"data/loader/flash/{H5_PATHS[1]}") copy_file_path = tmp_path / "copy2.h5" shutil.copyfile(original_file_path, copy_file_path) @@ -87,7 +86,4 @@ def fixture_h5_paths() -> list[Path]: Returns: list: A list of h5 file paths. """ - return [ - Path(os.path.join(package_dir, f"../../tests/data/loader/flash/{path}")) - for path in H5_PATHS - ] + return [Path(os.path.join(test_dir, f"data/loader/flash/{path}")) for path in H5_PATHS] diff --git a/tests/loader/mpes/test_mpes_loader.py b/tests/loader/mpes/test_mpes_loader.py index 0bc15b6f..d48a58d2 100644 --- a/tests/loader/mpes/test_mpes_loader.py +++ b/tests/loader/mpes/test_mpes_loader.py @@ -4,16 +4,14 @@ import logging import os from copy import deepcopy -from importlib.util import find_spec import pytest from sed.core.config import parse_config from sed.loader.mpes.loader import MpesLoader -package_dir = os.path.dirname(find_spec("sed").origin) - -test_data_dir = os.path.join(package_dir, "../../tests/data/loader/mpes") +test_dir = os.path.join(os.path.dirname(__file__), "../..") +test_data_dir = os.path.join(test_dir, "data/loader/mpes") config = parse_config( os.path.join(test_data_dir, "config.yaml"), diff --git a/tests/loader/sxp/test_sxp_loader.py b/tests/loader/sxp/test_sxp_loader.py index 8ae3f64a..669003c8 100644 --- a/tests/loader/sxp/test_sxp_loader.py +++ b/tests/loader/sxp/test_sxp_loader.py @@ -2,7 +2,6 @@ from __future__ import annotations import os -from importlib.util import find_spec from pathlib import Path import pytest @@ -10,8 +9,8 @@ from sed.core.config import parse_config from sed.loader.sxp.loader import SXPLoader -package_dir = os.path.dirname(find_spec("sed").origin) -config_path = os.path.join(package_dir, "../../tests/data/loader/sxp/config.yaml") +test_dir = os.path.join(os.path.dirname(__file__), "../..") +config_path = os.path.join(test_dir, "data/loader/sxp/config.yaml") H5_PATH = "RAW-R0016-DA03-S00000.h5" diff --git a/tests/loader/test_loaders.py b/tests/loader/test_loaders.py index 705f8ae8..a5b357d0 100644 --- a/tests/loader/test_loaders.py +++ b/tests/loader/test_loaders.py @@ -4,7 +4,6 @@ import os from copy import deepcopy -from importlib.util import find_spec from pathlib import Path from typing import cast @@ -19,9 +18,8 @@ from sed.loader.loader_interface import get_names_of_all_loaders from sed.loader.utils import gather_files -package_dir = os.path.dirname(find_spec("sed").origin) - -test_data_dir = os.path.join(package_dir, "../../tests/data") +test_dir = os.path.join(os.path.dirname(__file__), "..") +test_data_dir = os.path.join(test_dir, "data") read_types = ["one_file", "files", "one_folder", "folders", "one_run", "runs"] runs = {"generic": None, "mpes": ["30", "50"], "flash": ["43878", "43878"], "sxp": ["0016", "0016"]} diff --git a/tests/loader/test_mirrorutil.py b/tests/loader/test_mirrorutil.py index 5aa57477..e88b51ad 100644 --- a/tests/loader/test_mirrorutil.py +++ b/tests/loader/test_mirrorutil.py @@ -8,17 +8,16 @@ import shutil import tempfile from contextlib import redirect_stdout -from importlib.util import find_spec import pytest from sed.loader.mirrorutil import CopyTool -package_dir = os.path.dirname(find_spec("sed").origin) -source_folder = package_dir + "/../../" -folder = package_dir + "/../../tests/data/loader/mpes" -file = package_dir + "/../../tests/data/loader/mpes/Scan0030_2.h5" +test_dir = os.path.join(os.path.dirname(__file__), "..") +source_folder = test_dir + "/../" +folder = test_dir + "/data/loader/mpes" +file = test_dir + "/data/loader/mpes/Scan0030_2.h5" def test_copy_tool_folder() -> None: diff --git a/tests/test_config.py b/tests/test_config.py index d94a44f1..7f8a43b8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,6 @@ import copy import os import tempfile -from importlib.util import find_spec from pathlib import Path import pytest @@ -16,9 +15,8 @@ from sed.core.config import parse_config from sed.core.config import save_config -package_dir = os.path.dirname(find_spec("sed").origin) - -test_config_dir = Path(package_dir).joinpath("../tests/data/loader/") +test_dir = os.path.dirname(__file__) +test_config_dir = Path(f"{test_dir}/data/loader/") config_paths = test_config_dir.glob("*/*.yaml") default_config_keys = [ @@ -92,10 +90,10 @@ def test_load_does_not_modify() -> None: def test_load_config() -> None: """Test if the config loader can handle json and yaml files.""" config_json = load_config( - f"{package_dir}/../../tests/data/config/config.json", + f"{test_dir}/data/config/config.json", ) config_yaml = load_config( - f"{package_dir}/../../tests/data/config/config.yaml", + f"{test_dir}/data/config/config.yaml", ) assert config_json == config_yaml @@ -103,7 +101,7 @@ def test_load_config() -> None: def test_load_config_raise() -> None: """Test if the config loader raises an error for a wrong file type.""" with pytest.raises(TypeError): - load_config(f"{package_dir}/../../README.md") + load_config(f"{test_dir}/../README.md") def test_complete_dictionary() -> None: diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index e99fdc1e..24223460 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -5,7 +5,6 @@ import glob import itertools import os -from importlib.util import find_spec import pytest @@ -13,13 +12,12 @@ from sed.diagnostics import grid_histogram from sed.loader.loader_interface import get_loader -# pylint: disable=duplicate-code -package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../../tests/data/loader/mpes/" -folder = package_dir + "/../../tests/data/calibrator/" +test_dir = os.path.dirname(__file__) +df_folder = f"{test_dir}/data/loader/mpes/" +calibration_folder = f"{test_dir}/data/calibrator/" files = glob.glob(df_folder + "*.h5") config = parse_config( - package_dir + "/../../tests/data/loader/mpes/config.yaml", + f"{test_dir}/data/loader/mpes/config.yaml", folder_config={}, user_config={}, system_config={}, diff --git a/tests/test_processor.py b/tests/test_processor.py index 4a64d141..3cfe9bcf 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -24,29 +24,29 @@ from sed.core.config import parse_config from sed.loader.loader_interface import get_loader -# pylint: disable=duplicate-code package_dir = os.path.dirname(find_spec("sed").origin) -df_folder = package_dir + "/../../tests/data/loader/mpes/" -df_folder_generic = package_dir + "/../../tests/data/loader/generic/" -folder = package_dir + "/../../tests/data/calibrator/" +test_dir = os.path.dirname(__file__) +df_folder = f"{test_dir}/data/loader/mpes/" +df_folder_generic = f"{test_dir}/data/loader/generic/" +calibration_folder = f"{test_dir}/data/calibrator/" files = glob.glob(df_folder + "*.h5") runs = ["30", "50"] runs_flash = ["43878", "43878"] loader = get_loader(loader_name="mpes") -source_folder = package_dir + "/../../" +source_folder = f"{test_dir}/../" dest_folder = tempfile.mkdtemp() gid = os.getgid() traces_list = [] -with open(folder + "traces.csv", newline="", encoding="utf-8") as csvfile: +with open(calibration_folder + "traces.csv", newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile, quoting=csv.QUOTE_NONNUMERIC) for row in reader: traces_list.append(row) traces = np.asarray(traces_list).T -with open(folder + "tof.csv", newline="", encoding="utf-8") as csvfile: +with open(calibration_folder + "tof.csv", newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile, quoting=csv.QUOTE_NONNUMERIC) tof = np.asarray(next(reader)) -with open(folder + "biases.csv", newline="", encoding="utf-8") as csvfile: +with open(calibration_folder + "biases.csv", newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile, quoting=csv.QUOTE_NONNUMERIC) biases = np.asarray(next(reader)) From 208beeee3f52692f834b3896f4a17fd551031df3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 01:51:43 +0100 Subject: [PATCH 264/300] limit photutils --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 63104d3e..0a150b70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "numba>=0.55.1", "numpy>=1.18,<2.0", "pandas>=1.4.1", + "photutils<2.0", "psutil>=5.9.0", "pynxtools-mpes>=0.2.0", "pynxtools>=0.9.0", From 422a7f3fb3866cb1d5e9b668de1c14105243bdc3 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 01:56:56 +0100 Subject: [PATCH 265/300] remove 3.8 again --- .github/workflows/testing_multiversion.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index b46f9778..b8338086 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -19,7 +19,7 @@ jobs: # Using matrix strategy strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: # Check out repo and set up Python diff --git a/pyproject.toml b/pyproject.toml index 0a150b70..6a852d9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ authors = [ readme = "README.md" keywords = ["sed", "mpes", "flash", "arpes"] license = {text = "MIT"} -requires-python = ">=3.8,<3.13" +requires-python = ">=3.9,<3.13" dependencies = [ "bokeh>=2.4.2", "dask>=2021.12.0,<2024.8", From fd636504da3b62283924314fdb276f45a34a080c Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:32:41 +0100 Subject: [PATCH 266/300] fix coverage report --- .github/workflows/linting.yml | 2 ++ .github/workflows/testing_coverage.yml | 4 +++- .github/workflows/testing_multiversion.yml | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 2539e1cc..c58bdd16 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -15,6 +15,8 @@ jobs: steps: # Check out repo and set up Python - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Setup python - name: Set up Python 3.10 diff --git a/.github/workflows/testing_coverage.yml b/.github/workflows/testing_coverage.yml index 4fa12fd9..e15e6a86 100644 --- a/.github/workflows/testing_coverage.yml +++ b/.github/workflows/testing_coverage.yml @@ -18,6 +18,8 @@ jobs: steps: # Check out repo and set up Python - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Setup python - name: Set up Python 3.10 @@ -31,7 +33,7 @@ jobs: - name: Install package run: | - uv pip install ".[dev]" + uv pip install -e ".[dev]" # Run pytest with coverage report, saving to xml - name: Run tests on python 3.10 diff --git a/.github/workflows/testing_multiversion.yml b/.github/workflows/testing_multiversion.yml index b8338086..4f788331 100644 --- a/.github/workflows/testing_multiversion.yml +++ b/.github/workflows/testing_multiversion.yml @@ -24,6 +24,8 @@ jobs: steps: # Check out repo and set up Python - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From d55e176f1fb913aab361e41a4f4c007d4e224aae Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:37:55 +0100 Subject: [PATCH 267/300] add build step --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 842ed8fe..3fb4c811 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,11 @@ jobs: curl -LsSf https://astral.sh/uv/install.sh | sh uv pip install build + - name: Build package + run: | + git reset --hard HEAD + python -m build + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 #with: From 3c241c8f657ba1c1f2920517bac1ba8cedd8d1c0 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:43:58 +0100 Subject: [PATCH 268/300] try fix release --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fb4c811..e16dfea6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,11 +44,12 @@ jobs: uv pip install build - name: Build package + working-directory: sed-processor run: | git reset --hard HEAD python -m build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - #with: - # packages-dir: . + with: + packages-dir: sed-processor/dist From dee2501fd0ee20154238bc0b42c511408a5d53be Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:49:07 +0100 Subject: [PATCH 269/300] fix benchmark --- .github/workflows/benchmark.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 13cb9711..f3cda9a5 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,6 +18,8 @@ jobs: # Check out repo and set up Python - name: Check out the repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: tibdex/github-app-token@v1 id: generate-token @@ -37,7 +39,7 @@ jobs: - name: Install package run: | - uv pip install "." + uv pip install ".[dev]" # Run benchmarks - name: Run benchmarks on python 3.10 From bfca5bd974787e38bf02e86c49ddc603b5ab8e62 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:51:58 +0100 Subject: [PATCH 270/300] fix url --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e16dfea6..6a572f96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest environment: name: test-pypi - url: https://test-pypi.org/p/sed-processor + url: https://test.pypi.org/p/sed-processor permissions: id-token: write From d868238be587fe43b64faa7678c1b758bef76bf1 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 09:57:26 +0100 Subject: [PATCH 271/300] add pypi-test url and credentials --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a572f96..c9f89b7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,4 +52,6 @@ jobs: - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.PYPI_TEST }} packages-dir: sed-processor/dist From 469b7b7eac8278bdc9e5ace079597416cb24123b Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 10:03:53 +0100 Subject: [PATCH 272/300] fix scripts --- benchmarks/benchmark_sed.py | 17 +++++++++-------- docs/scripts/build_flash_parquets.py | 2 +- docs/scripts/build_sxp_parquets.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/benchmarks/benchmark_sed.py b/benchmarks/benchmark_sed.py index 74c8de23..a8d82e69 100644 --- a/benchmarks/benchmark_sed.py +++ b/benchmarks/benchmark_sed.py @@ -18,6 +18,7 @@ from tests.loader.test_loaders import get_loader_name_from_loader_object package_dir = os.path.dirname(find_spec("sed").origin) +benchmark_dir = os.path.dirname(__file__) num_cores = min(20, psutil.cpu_count()) @@ -32,10 +33,10 @@ ) dataframe = dask.dataframe.from_dask_array(array, columns=axes) -test_data_dir = os.path.join(package_dir, "..", "tests", "data") +test_data_dir = os.path.join(benchmark_dir, "..", "tests", "data") runs = {"generic": None, "mpes": ["30", "50"], "flash": ["43878"], "sxp": ["0016"]} -targets = load_config(package_dir + "/../../benchmarks/benchmark_targets.yaml") +targets = load_config(benchmark_dir + "/benchmark_targets.yaml") def test_binning_1d() -> None: @@ -59,7 +60,7 @@ def test_binning_1d() -> None: if np.mean(result) < 0.8 * targets["binning_1d"]: print(f"Updating targets for 'binning_1d' to {float(np.mean(result))}") targets["binning_1d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") def test_binning_4d() -> None: @@ -83,7 +84,7 @@ def test_binning_4d() -> None: if np.mean(result) < 0.8 * targets["binning_4d"]: print(f"Updating targets for 'binning_4d' to {float(np.mean(result))}") targets["binning_4d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") def test_splinewarp() -> None: @@ -109,7 +110,7 @@ def test_splinewarp() -> None: if np.mean(result) < 0.8 * targets["inv_dfield"]: print(f"Updating targets for 'inv_dfield' to {float(np.mean(result))}") targets["inv_dfield"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") def test_workflow_1d() -> None: @@ -145,7 +146,7 @@ def test_workflow_1d() -> None: if np.mean(result) < 0.8 * targets["workflow_1d"]: print(f"Updating targets for 'workflow_1d' to {float(np.mean(result))}") targets["workflow_1d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") def test_workflow_4d() -> None: @@ -181,7 +182,7 @@ def test_workflow_4d() -> None: if np.mean(result) < 0.8 * targets["workflow_4d"]: print(f"Updating targets for 'workflow_4d' to {float(np.mean(result))}") targets["workflow_4d"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") @pytest.mark.parametrize("loader", get_all_loaders()) @@ -210,4 +211,4 @@ def test_loader_compute(loader: BaseLoader) -> None: f"to {float(np.mean(result))}", ) targets[f"loader_compute_{loader_name}"] = float(np.mean(result)) - save_config(targets, package_dir + "/../../benchmarks/benchmark_targets.yaml") + save_config(targets, benchmark_dir + "/benchmark_targets.yaml") diff --git a/docs/scripts/build_flash_parquets.py b/docs/scripts/build_flash_parquets.py index 004cc0af..eef402b3 100644 --- a/docs/scripts/build_flash_parquets.py +++ b/docs/scripts/build_flash_parquets.py @@ -6,7 +6,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -config_file = package_dir + "/../../src/sed/config/flash_example_config.yaml" +config_file = package_dir + "/config/flash_example_config.yaml" dataset.get("Gd_W110", root_dir="./tutorial") data_path = dataset.dir diff --git a/docs/scripts/build_sxp_parquets.py b/docs/scripts/build_sxp_parquets.py index ab712dec..5dcbcc74 100644 --- a/docs/scripts/build_sxp_parquets.py +++ b/docs/scripts/build_sxp_parquets.py @@ -6,7 +6,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) -config_file = package_dir + "/../../src/sed/config/sxp_example_config.yaml" +config_file = package_dir + "/config/sxp_example_config.yaml" dataset.get("Au_Mica", root_dir="./tutorial") data_path = dataset.dir From 1ca5acf189860bef10a9c11e66c93e5e158f638e Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 10:09:34 +0100 Subject: [PATCH 273/300] remove password --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9f89b7d..ff62e053 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,5 +53,4 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.PYPI_TEST }} packages-dir: sed-processor/dist From ead12562b2f124eacfb8bb7c841d82249ec0f3d2 Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 10:11:37 +0100 Subject: [PATCH 274/300] add verbose output --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff62e053..97026d3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,6 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.PYPI_TEST }} + verbose: true packages-dir: sed-processor/dist From 6452e2b1ffcb61965f6a36450d7c44d97c7cc91a Mon Sep 17 00:00:00 2001 From: rettigl Date: Thu, 2 Jan 2025 10:33:05 +0100 Subject: [PATCH 275/300] fix docs config --- docs/conf.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 68ec181c..6c7b066f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,8 @@ import tomlkit +from sed import __version__ + sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- @@ -22,7 +24,7 @@ def _get_project_meta(): with open("../pyproject.toml") as pyproject: file_contents = pyproject.read() - return tomlkit.parse(file_contents)["tool"]["poetry"] + return tomlkit.parse(file_contents)["project"] # -- Project information ----------------------------------------------------- @@ -32,7 +34,7 @@ def _get_project_meta(): author = "OpenCOMPES team" # The short X.Y version -version = str(pkg_meta["version"]) +version = __version__ # The full version, including alpha/beta/rc tags release = version @@ -93,7 +95,7 @@ def _get_project_meta(): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] # -- Options for HTML output ------------------------------------------------- From e30c65c35b21a95f13d969fec1281dc725d10c1d Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 4 Jan 2025 00:32:00 +0100 Subject: [PATCH 276/300] publish to main pypi, and remove trigger --- .github/workflows/release.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97026d3d..def39ef9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,8 +5,6 @@ name: Publish to PyPI on: release: types: [published] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. @@ -22,8 +20,8 @@ jobs: name: Upload release to PyPI runs-on: ubuntu-latest environment: - name: test-pypi - url: https://test.pypi.org/p/sed-processor + name: pypi + url: https://pypi.org/p/sed-processor permissions: id-token: write @@ -52,7 +50,5 @@ jobs: - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.PYPI_TEST }} verbose: true packages-dir: sed-processor/dist From f83189827af848dbd236b5f2ebeb526ba42b777e Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 4 Jan 2025 00:58:47 +0100 Subject: [PATCH 277/300] update docs --- README.md | 2 +- docs/misc/contributing.rst | 29 +++++++++++++++++++---------- docs/misc/maintain.rst | 20 ++++++++------------ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index df51b61e..939e6ffa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Finally, in contains several export routines, including export into the [NeXus]( # Installation ## Prerequisites -- Python 3.8+ +- Python 3.9+ - pip ## Steps diff --git a/docs/misc/contributing.rst b/docs/misc/contributing.rst index 7aab314a..62d4ea56 100644 --- a/docs/misc/contributing.rst +++ b/docs/misc/contributing.rst @@ -28,13 +28,13 @@ Getting Started -2. **Install Python and Poetry:** - - Ensure you have Python 3.8, 3.9, 3.10 or 3.11 and poetry installed. +2. **Install uv and Python:** + - Ensure you have uv and Python 3.9, 3.10, 3.11 or 3.12 installed. You can install it e.g. using the following commands: .. code-block:: bash - pip install pipx - pipx install poetry + curl -LsSf https://astral.sh/uv/install.sh | sh + uv python install 3.10 3. **Clone Repository:** @@ -42,25 +42,34 @@ Getting Started git clone https://github.com/OpenCOMPES/sed.git -4. **Install Dependencies:** - - Navigate to the project directory and install the project dependencies (including development ones) using Poetry: +4. **Set up virtual environment:** + - Create a python virtual environment, and activate it. You can optionally select the python version, and set the path .. code-block:: bash - poetry install --dev + uv venv -p=3.10 .venv + source .venv/bin/activate + + +5. **Install Dependencies:** + - Navigate to the project directory and install the project and its dependencies (including development ones) in "editable" mode using uv. Optionally, the jupyter notebook can be installed as well: + + .. code-block:: bash + + uv pip install -e .[dev,notebook] Development Workflow ===================== .. note:: - This guide assumes that you have Python (version 3.8, 3.9, 3.10, 3.11) and poetry with dev dependencies installed on your machine. + This guide assumes that you followed the previous steps and have uv, python and the package with dev dependencies installed on your machine. 1. **Install pre-commit hooks:** To ensure your code is formatted correctly, install pre-commit hooks: .. code-block:: bash - pip install pre-commit + pre-commit install 2. **Create a Branch:** Create a new branch for your feature or bug fix and make changes: @@ -86,7 +95,7 @@ Development Workflow git commit -a -m "Your commit message" -6. **Push Changes:** Push your changes to your fork: +6. **Push Changes:** Push your changes to your new branch: .. code-block:: bash diff --git a/docs/misc/maintain.rst b/docs/misc/maintain.rst index cf8d0a93..6ddd91fe 100644 --- a/docs/misc/maintain.rst +++ b/docs/misc/maintain.rst @@ -11,8 +11,8 @@ Users can generate documentation locally using the following steps: .. code-block:: bash - pip install pipx - pipx install poetry + curl -LsSf https://astral.sh/uv/install.sh | sh + uv python install 3.10 1. **Clone Repository:** @@ -39,19 +39,20 @@ Doing this step will slow down the build process significantly. It also requires .. code-block:: bash - poetry shell + uv venv -p=3.10 .venv + source .venv/bin/activate 6. **Install Dependencies:** .. code-block:: bash - poetry install --with docs + uv pip install -e .[docs] 7. **Build Documentation:** .. code-block:: bash - poetry run sphinx-build -b html docs _build + sphinx-build -b html docs _build 8. **View Documentation:** @@ -147,10 +148,5 @@ To create a release, follow these steps: - *Release Job:* - This workflow is responsible for versioning and releasing the package. - - A release job runs on every git tag push (e.g., `git tag v0.1.5`) and publishes the package to PyPI. - - If the publish is successful, the version in the `pyproject.toml` file is updated and pushed to the main branch. - -- *Prerelease Job:* - - This workflow is triggered automatically on every pull request (PR) to the main branch. - - It increments the version number for prerelease (e.g., from 0.1.5 to 0.1.6a0 to 0.1.6a1) and publishes the package to PyPI. - - If the publish is successful, the version in the `pyproject.toml` file is updated and pushed to the main branch. + - A release job runs on every git release and publishes the package to PyPI. + - The package version is dynamically obtained from the most recent git tag. From 64b07c168672f962a4589978d6e07d2d0878b017 Mon Sep 17 00:00:00 2001 From: rettigl Date: Sat, 4 Jan 2025 10:35:24 +0100 Subject: [PATCH 278/300] further docs fixes --- docs/misc/contributing.rst | 9 +++------ docs/misc/maintain.rst | 6 +++--- docs/user_guide/installation.md | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/misc/contributing.rst b/docs/misc/contributing.rst index 62d4ea56..d638deef 100644 --- a/docs/misc/contributing.rst +++ b/docs/misc/contributing.rst @@ -28,8 +28,7 @@ Getting Started -2. **Install uv and Python:** - - Ensure you have uv and Python 3.9, 3.10, 3.11 or 3.12 installed. You can install it e.g. using the following commands: +2. **Install uv and Python:** Ensure you have uv and Python 3.9, 3.10, 3.11 or 3.12 installed. You can install it e.g. using the following commands: .. code-block:: bash @@ -42,8 +41,7 @@ Getting Started git clone https://github.com/OpenCOMPES/sed.git -4. **Set up virtual environment:** - - Create a python virtual environment, and activate it. You can optionally select the python version, and set the path +4. **Set up virtual environment:** Create a python virtual environment, and activate it. You can optionally select the python version, and set the path .. code-block:: bash @@ -51,8 +49,7 @@ Getting Started source .venv/bin/activate -5. **Install Dependencies:** - - Navigate to the project directory and install the project and its dependencies (including development ones) in "editable" mode using uv. Optionally, the jupyter notebook can be installed as well: +5. **Install Dependencies:** Navigate to the project directory and install the project and its dependencies (including development ones) in "editable" mode using uv. Optionally, the jupyter notebook can be installed as well: .. code-block:: bash diff --git a/docs/misc/maintain.rst b/docs/misc/maintain.rst index 6ddd91fe..b7e2068a 100644 --- a/docs/misc/maintain.rst +++ b/docs/misc/maintain.rst @@ -105,14 +105,14 @@ Here's how the workflow works: - The workflow is divided into two jobs: build and deploy. a. **Build Job:** - - Sets up the build environment, checks out the repository, and installs necessary dependencies using Poetry. + - Sets up the build environment, checks out the repository, and installs necessary dependencies using uv. - Installs notebook dependencies and Pandoc. - - Copies tutorial files to the docs directory and removes unnecessary notebooks. + - Copies tutorial files to the docs directory. - Downloads RAW data for tutorials. - Builds Sphinx documentation. b. **Deploy Job:** - - Deploys the built documentation to GitHub Pages. + - Deploys the built documentation to GitHub Pages repository. 5. **Manual Execution:** - To manually trigger the workflow, go to the Actions tab on GitHub. diff --git a/docs/user_guide/installation.md b/docs/user_guide/installation.md index 8c3f44dd..44d68601 100644 --- a/docs/user_guide/installation.md +++ b/docs/user_guide/installation.md @@ -23,10 +23,10 @@ source .sed-venv/bin/activate - Install `sed`, distributed as `sed-processor` on PyPI: ```bash -pip install sed-processor[all] +pip install sed-processor[notebook] ``` -- If you do not use Jupyter Notebook or Jupyter Lab, you can skip the installing those dependencies: +- If you do not use Jupyter Notebook or Jupyter Lab, you can skip the installation of those dependencies: ```bash pip install sed-processor From 8d9bcd50cdc24270ae4e16f4f69589add8a63359 Mon Sep 17 00:00:00 2001 From: rettigl Date: Tue, 7 Jan 2025 00:50:01 +0100 Subject: [PATCH 279/300] remove empty pulses from timed dataframe, and bring back old behavior --- src/sed/loader/flash/buffer_handler.py | 2 +- tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb | 4 ++-- ...11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb | 4 ++-- tutorial/9_hextof_workflow_trXPD.ipynb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index b335dd01..11d642f7 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -187,7 +187,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: # timed dataframe # drop the electron channels and only take rows with the first electronId - df_timed = df[self.fill_channels].loc[:, :, 0] + df_timed = df.dropna(subset=electron_channels)[self.fill_channels].loc[:, :, 0] dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) logger.debug(f"Processed {paths['raw'].stem}") diff --git a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb index 0054aa4a..3f4c8630 100644 --- a/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb +++ b/tutorial/10_hextof_workflow_trXPS_bam_correction.ipynb @@ -228,7 +228,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [1446.75,1449.15]]\n", "bins = [200,40]\n", - "res = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" + "res = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" ] }, { @@ -394,7 +394,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" ] }, { diff --git a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb index 40831f8f..4d3fbe40 100644 --- a/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb +++ b/tutorial/11_hextof_workflow_trXPS_energy_calibration_using_SB.ipynb @@ -325,7 +325,7 @@ "axes = ['dldTimeSteps', 'delayStage']\n", "ranges = [[3900,4200], [-1.5,1.5]]\n", "bins = [100,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)\n", + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")\n", "\n", "fig,ax = plt.subplots(1,2,figsize=(8,3), layout='constrained')\n", "fig.suptitle(f\"Run {run_number}: W 4f, side bands\")\n", @@ -441,7 +441,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)\n", + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")\n", "\n", "fig,ax = plt.subplots(1,2,figsize=(8,3), layout='constrained')\n", "fig.suptitle(f\"Run {run_number}: W 4f, side bands\")\n", diff --git a/tutorial/9_hextof_workflow_trXPD.ipynb b/tutorial/9_hextof_workflow_trXPD.ipynb index 98f1f074..c5e93408 100644 --- a/tutorial/9_hextof_workflow_trXPD.ipynb +++ b/tutorial/9_hextof_workflow_trXPD.ipynb @@ -281,7 +281,7 @@ "axes = ['energy', 'delayStage']\n", "ranges = [[-37.5,-27.5], [-1.5,1.5]]\n", "bins = [200,60]\n", - "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges)" + "res_corr = sp_44498.compute(bins=bins, axes=axes, ranges=ranges, normalize_to_acquisition_time=\"delayStage\")" ] }, { From 39c2e5279cc437f30c39a941274c606b8c1d13da Mon Sep 17 00:00:00 2001 From: Laurenz Rettig Date: Tue, 7 Jan 2025 17:03:49 +0100 Subject: [PATCH 280/300] add further exceptions for completely empty files, and exceptions handling for filesystems without proper file xattributes --- src/sed/loader/mirrorutil.py | 5 ++++- src/sed/loader/mpes/loader.py | 36 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/sed/loader/mirrorutil.py b/src/sed/loader/mirrorutil.py index 0c760e7f..ed7d3df4 100644 --- a/src/sed/loader/mirrorutil.py +++ b/src/sed/loader/mirrorutil.py @@ -368,7 +368,10 @@ def mycopy(source: str, dest: str, gid: int, mode: int, replace: bool = False): if replace: if os.path.exists(dest): os.remove(dest) - shutil.copy2(source, dest) + try: + shutil.copy2(source, dest) + except OSError: + shutil.copy(source, dest) # fix permissions and group ownership: os.chown(dest, -1, gid) os.chmod(dest, mode) diff --git a/src/sed/loader/mpes/loader.py b/src/sed/loader/mpes/loader.py index 92c64b05..e3e75cda 100644 --- a/src/sed/loader/mpes/loader.py +++ b/src/sed/loader/mpes/loader.py @@ -159,7 +159,12 @@ def hdf5_to_dataframe( for name, channel in channels.items(): if channel["format"] == "per_file": if channel["dataset_key"] in test_proc.attrs: - values = [float(get_attribute(h5py.File(f), channel["dataset_key"])) for f in files] + values = [] + for f in files: + try: + values.append(float(get_attribute(h5py.File(f), channel["dataset_key"]))) + except OSError: + pass delayeds = [ add_value(partition, name, value) for partition, value in zip(dataframe.partitions, values) @@ -274,7 +279,12 @@ def hdf5_to_timed_dataframe( for name, channel in channels.items(): if channel["format"] == "per_file": if channel["dataset_key"] in test_proc.attrs: - values = [float(get_attribute(h5py.File(f), channel["dataset_key"])) for f in files] + values = [] + for f in files: + try: + values.append(float(get_attribute(h5py.File(f), channel["dataset_key"]))) + except OSError: + pass delayeds = [ add_value(partition, name, value) for partition, value in zip(dataframe.partitions, values) @@ -843,11 +853,23 @@ def get_start_and_end_time(self) -> tuple[float, float]: ) ts_from = timestamps[-1][1] h5filename = self.files[-1] - timestamps = hdf5_to_array( - h5filename=h5filename, - channels=channels, - time_stamps=True, - ) + try: + timestamps = hdf5_to_array( + h5filename=h5filename, + channels=channels, + time_stamps=True, + ) + except OSError: + try: + h5filename = self.files[-2] + timestamps = hdf5_to_array( + h5filename=h5filename, + channels=channels, + time_stamps=True, + ) + except OSError: + ts_to = ts_from + logger.warning("Could not read end time, using start time as end time!") ts_to = timestamps[-1][-1] return (ts_from, ts_to) From b4ddac44d9cc70a0c7ec7103c996111b6195bfe3 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:21:27 +0100 Subject: [PATCH 281/300] allow both timed dataframe formats --- src/sed/loader/flash/buffer_handler.py | 46 ++++++++++++++++++-------- src/sed/loader/flash/loader.py | 5 +++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index 11d642f7..81d7bc5a 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -130,6 +130,7 @@ def __init__( extend_aux=True, ) self.metadata: dict = {} + self.filter_timed_by_electron: bool = None def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: """ @@ -159,37 +160,50 @@ def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: "Please check the configuration file or set force_recreate to True.", ) - def _save_buffer_file(self, paths: dict[str, Path]) -> None: - """ - Creates the electron and timed buffer files from the raw H5 file. - First the dataframe is accessed and forward filled in the non-electron channels. - Then the data types are set. For the electron dataframe, all values not in the electron - channels are dropped. For the timed dataframe, only the train and pulse channels are taken - and it pulse resolved (no longer electron resolved). Both are saved as parquet files. + def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: + """Creates the timed dataframe, optionally filtering by electron events. Args: - paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. + df (dd.DataFrame): The input dataframe containing all data + + Returns: + dd.DataFrame: The timed dataframe """ + # Get channels that should be in timed dataframe + timed_channels = self.fill_channels - # Create a DataFrameCreator instance and the h5 file + if self.filter_timed_by_electron: + # Get electron channels to use for filtering + electron_channels = get_channels(self._config, "per_electron") + # Filter rows where electron data exists + df_timed = df.dropna(subset=electron_channels)[timed_channels] + else: + # Take all timed data rows without filtering + df_timed = df[timed_channels] + + # Take only first electron per event + return df_timed.loc[:, :, 0] + + def _save_buffer_file(self, paths: dict[str, Path]) -> None: + """Creates the electron and timed buffer files from the raw H5 file.""" + # Create a DataFrameCreator instance and get the h5 file df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df # forward fill all the non-electron channels df[self.fill_channels] = df[self.fill_channels].ffill() - # Reset the index of the DataFrame and save both the electron and timed dataframes - # electron resolved dataframe + # Save electron resolved dataframe electron_channels = get_channels(self._config, "per_electron") dtypes = get_dtypes(self._config, df.columns.values) df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( paths["electron"], ) - # timed dataframe - # drop the electron channels and only take rows with the first electronId - df_timed = df.dropna(subset=electron_channels)[self.fill_channels].loc[:, :, 0] + # Create and save timed dataframe + df_timed = self._create_timed_dataframe(df) dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) + logger.debug(f"Processed {paths['raw'].stem}") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: @@ -272,6 +286,7 @@ def process_and_load_dataframe( suffix: str = "", debug: bool = False, remove_invalid_files: bool = False, + filter_timed_by_electron: bool = True, ) -> tuple[dd.DataFrame, dd.DataFrame]: """ Runs the buffer file creation process. @@ -284,11 +299,14 @@ def process_and_load_dataframe( force_recreate (bool): Flag to force recreation of buffer files. suffix (str): Suffix for buffer file names. debug (bool): Flag to enable debug mode.): + remove_invalid_files (bool): Flag to remove invalid files. + filter_timed_by_electron (bool): Flag to filter timed data by valid electron events. Returns: Tuple[dd.DataFrame, dd.DataFrame]: The electron and timed dataframes. """ self.fp = BufferFilePaths(self._config, h5_paths, folder, suffix, remove_invalid_files) + self.filter_timed_by_electron = filter_timed_by_electron if not force_recreate: schema_set = set( diff --git a/src/sed/loader/flash/loader.py b/src/sed/loader/flash/loader.py index 6c84fc34..3d03f60d 100644 --- a/src/sed/loader/flash/loader.py +++ b/src/sed/loader/flash/loader.py @@ -303,6 +303,7 @@ def read_dataframe( ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, + filter_timed_by_electron: bool = True, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -319,6 +320,9 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. + filter_timed_by_electron (bool, optional): When True, the timed dataframe will only + contain data points where valid electron events were detected. When False, all + timed data points are included regardless of electron detection. Defaults to True. Keyword Args: detector (str, optional): The detector to use. Defaults to "". @@ -391,6 +395,7 @@ def read_dataframe( suffix=detector, debug=debug, remove_invalid_files=remove_invalid_files, + filter_timed_by_electron=filter_timed_by_electron, ) if self.instrument == "wespe": From 73269e5820130fcde18d4cf64655ffe0a551dee3 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:26:04 +0100 Subject: [PATCH 282/300] add back docstring --- src/sed/loader/flash/buffer_handler.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index 81d7bc5a..725f9a8a 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -185,7 +185,16 @@ def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: return df_timed.loc[:, :, 0] def _save_buffer_file(self, paths: dict[str, Path]) -> None: - """Creates the electron and timed buffer files from the raw H5 file.""" + """ + Creates the electron and timed buffer files from the raw H5 file. + First the dataframe is accessed and forward filled in the non-electron channels. + Then the data types are set. For the electron dataframe, all values not in the electron + channels are dropped. For the timed dataframe, only the train and pulse channels are taken + and it pulse resolved (no longer electron resolved). Both are saved as parquet files. + + Args: + paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. + """ # Create a DataFrameCreator instance and get the h5 file df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df From 87fa1fa99c8a1d29498a6b395715203eac514956 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:27:11 +0100 Subject: [PATCH 283/300] reformat --- src/sed/loader/flash/buffer_handler.py | 2 +- src/sed/loader/flash/loader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index 725f9a8a..a1e5a258 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -212,7 +212,7 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: df_timed = self._create_timed_dataframe(df) dtypes = get_dtypes(self._config, df_timed.columns.values) df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) - + logger.debug(f"Processed {paths['raw'].stem}") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: diff --git a/src/sed/loader/flash/loader.py b/src/sed/loader/flash/loader.py index 3d03f60d..40106431 100644 --- a/src/sed/loader/flash/loader.py +++ b/src/sed/loader/flash/loader.py @@ -320,8 +320,8 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. - filter_timed_by_electron (bool, optional): When True, the timed dataframe will only - contain data points where valid electron events were detected. When False, all + filter_timed_by_electron (bool, optional): When True, the timed dataframe will only + contain data points where valid electron events were detected. When False, all timed data points are included regardless of electron detection. Defaults to True. Keyword Args: From 299d04bd99a9fc573c0e9a8f6befe26c5d11b61c Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:07:44 +0100 Subject: [PATCH 284/300] fix metadata issues, and add more debug logs --- src/sed/loader/flash/buffer_handler.py | 62 +++++++++++++++-------- src/sed/loader/flash/dataframe.py | 20 ++++++-- src/sed/loader/flash/metadata.py | 40 ++++++++------- tests/loader/flash/test_flash_metadata.py | 16 +++--- 4 files changed, 86 insertions(+), 52 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index a1e5a258..b7cab37e 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -2,6 +2,7 @@ import os from pathlib import Path +import time import dask.dataframe as dd import pyarrow.parquet as pq @@ -20,7 +21,7 @@ DF_TYP = ["electron", "timed"] -logger = setup_logging(__name__) +logger = setup_logging("flash_buffer_handler") class BufferFilePaths: @@ -135,16 +136,15 @@ def __init__( def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: """ Checks the schema of the Parquet files. - - Raises: - ValueError: If the schema of the Parquet files does not match the configuration. """ + logger.debug(f"Checking schema for {len(files)} files") existing = [file for file in files if file.exists()] parquet_schemas = [pq.read_schema(file) for file in existing] for filename, schema in zip(existing, parquet_schemas): schema_set = set(schema.names) if schema_set != expected_schema_set: + logger.error(f"Schema mismatch in file: {filename}") missing_in_parquet = expected_schema_set - schema_set missing_in_config = schema_set - expected_schema_set @@ -159,6 +159,7 @@ def _schema_check(self, files: list[Path], expected_schema_set: set) -> None: f"{' '.join(errors)}. " "Please check the configuration file or set force_recreate to True.", ) + logger.debug("Schema check passed successfully") def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: """Creates the timed dataframe, optionally filtering by electron events. @@ -184,36 +185,57 @@ def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: # Take only first electron per event return df_timed.loc[:, :, 0] - def _save_buffer_file(self, paths: dict[str, Path]) -> None: - """ - Creates the electron and timed buffer files from the raw H5 file. - First the dataframe is accessed and forward filled in the non-electron channels. - Then the data types are set. For the electron dataframe, all values not in the electron - channels are dropped. For the timed dataframe, only the train and pulse channels are taken - and it pulse resolved (no longer electron resolved). Both are saved as parquet files. + def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: + """Creates the timed dataframe, optionally filtering by electron events. Args: - paths (dict[str, Path]): Dictionary containing the paths to the H5 and buffer files. + df (dd.DataFrame): The input dataframe containing all data + + Returns: + dd.DataFrame: The timed dataframe """ - # Create a DataFrameCreator instance and get the h5 file + # Get channels that should be in timed dataframe + timed_channels = self.fill_channels + + if self.filter_timed_by_electron: + # Get electron channels to use for filtering + electron_channels = get_channels(self._config, "per_electron") + # Filter rows where electron data exists + df_timed = df.dropna(subset=electron_channels)[timed_channels] + else: + # Take all timed data rows without filtering + df_timed = df[timed_channels] + + # Take only first electron per event + return df_timed.loc[:, :, 0] + + def _save_buffer_file(self, paths: dict[str, Path]) -> None: + """Creates the electron and timed buffer files from the raw H5 file.""" + logger.debug(f"Processing file: {paths['raw'].stem}") + start_time = time.time() + # Create DataFrameCreator and get get dataframe df = DataFrameCreator(config_dataframe=self._config, h5_path=paths["raw"]).df - # forward fill all the non-electron channels + # Forward fill non-electron channels + logger.debug(f"Forward filling {len(self.fill_channels)} channels") df[self.fill_channels] = df[self.fill_channels].ffill() # Save electron resolved dataframe electron_channels = get_channels(self._config, "per_electron") dtypes = get_dtypes(self._config, df.columns.values) - df.dropna(subset=electron_channels).astype(dtypes).reset_index().to_parquet( - paths["electron"], - ) + electron_df = df.dropna(subset=electron_channels).astype(dtypes).reset_index() + logger.debug(f"Saving electron buffer with shape: {electron_df.shape}") + electron_df.to_parquet(paths["electron"]) # Create and save timed dataframe df_timed = self._create_timed_dataframe(df) dtypes = get_dtypes(self._config, df_timed.columns.values) - df_timed.astype(dtypes).reset_index().to_parquet(paths["timed"]) + timed_df = df_timed.astype(dtypes).reset_index() + logger.debug(f"Saving timed buffer with shape: {timed_df.shape}") + timed_df.to_parquet(paths["timed"]) - logger.debug(f"Processed {paths['raw'].stem}") + + logger.debug(f"Processed {paths['raw'].stem} in {time.time() - start_time:.2f}s") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: """ @@ -286,7 +308,6 @@ def _get_dataframes(self) -> None: config=self._config, ) self.metadata.update(meta) - def process_and_load_dataframe( self, h5_paths: list[Path], @@ -337,3 +358,4 @@ def process_and_load_dataframe( self._get_dataframes() return self.df["electron"], self.df["timed"] + diff --git a/src/sed/loader/flash/dataframe.py b/src/sed/loader/flash/dataframe.py index 6501c82a..b8a5bfcc 100644 --- a/src/sed/loader/flash/dataframe.py +++ b/src/sed/loader/flash/dataframe.py @@ -14,6 +14,9 @@ from sed.loader.flash.utils import get_channels from sed.loader.flash.utils import InvalidFileError +from sed.core.logging import setup_logging + +logger = setup_logging("flash_dataframe_creator") class DataFrameCreator: @@ -34,6 +37,7 @@ def __init__(self, config_dataframe: dict, h5_path: Path) -> None: config_dataframe (dict): The configuration dictionary with only the dataframe key. h5_path (Path): Path to the h5 file. """ + logger.debug(f"Initializing DataFrameCreator for file: {h5_path}") self.h5_file = h5py.File(h5_path, "r") self.multi_index = get_channels(index=True) self._config = config_dataframe @@ -76,6 +80,7 @@ def get_dataset_array( tuple[pd.Index, np.ndarray | h5py.Dataset]: A tuple containing the train ID pd.Index and the channel's data. """ + logger.debug(f"Getting dataset array for channel: {channel}") # Get the data from the necessary h5 file and channel index_key, dataset_key = self.get_index_dataset_key(channel) @@ -85,6 +90,7 @@ def get_dataset_array( if slice_: slice_index = self._config["channels"][channel].get("slice", None) if slice_index is not None: + logger.debug(f"Slicing dataset with index: {slice_index}") dataset = np.take(dataset, slice_index, axis=1) # If np_array is size zero, fill with NaNs, fill it with NaN values # of the same shape as index @@ -291,10 +297,14 @@ def df(self) -> pd.DataFrame: Returns: pd.DataFrame: The combined pandas DataFrame. """ - + logger.debug("Creating combined DataFrame") self.validate_channel_keys() - # been tested with merge, join and concat - # concat offers best performance, almost 3 times faster + df = pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() - # all the negative pulse values are dropped as they are invalid - return df[df.index.get_level_values("pulseId") >= 0] + logger.debug(f"Created DataFrame with shape: {df.shape}") + + # Filter negative pulse values + df = df[df.index.get_level_values("pulseId") >= 0] + logger.debug(f"Filtered DataFrame shape: {df.shape}") + + return df diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index 50fd69b1..2317b006 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -7,6 +7,9 @@ import warnings import requests +from sed.core.logging import setup_logging + +logger = setup_logging("flash_metadata_retriever") class MetadataRetriever: @@ -15,19 +18,19 @@ class MetadataRetriever: on beamtime and run IDs. """ - def __init__(self, metadata_config: dict, scicat_token: str = None) -> None: + def __init__(self, metadata_config: dict, token: str = None) -> None: """ Initializes the MetadataRetriever class. Args: metadata_config (dict): Takes a dict containing at least url, and optionally token for the scicat instance. - scicat_token (str, optional): The token to use for fetching metadata. + token (str, optional): The token to use for fetching metadata. """ - self.token = metadata_config.get("scicat_token", None) - if scicat_token: - self.token = scicat_token - self.url = metadata_config.get("scicat_url", None) + self.token = metadata_config.get("token", None) + if token: + self.token = token + self.url = metadata_config.get("archiver_url", None) if not self.token or not self.url: raise ValueError("No URL or token provided for fetching metadata from scicat.") @@ -36,7 +39,7 @@ def __init__(self, metadata_config: dict, scicat_token: str = None) -> None: "Content-Type": "application/json", "Accept": "application/json", } - self.token = metadata_config["scicat_token"] + self.token = metadata_config["token"] def get_metadata( self, @@ -59,19 +62,18 @@ def get_metadata( Raises: Exception: If the request to retrieve metadata fails. """ - # If metadata is not provided, initialize it as an empty dictionary + logger.debug(f"Fetching metadata for beamtime {beamtime_id}, runs: {runs}") + if metadata is None: metadata = {} - # Iterate over the list of runs for run in runs: pid = f"{beamtime_id}/{run}" - # Retrieve metadata for each run and update the overall metadata dictionary + logger.debug(f"Retrieving metadata for PID: {pid}") metadata_run = self._get_metadata_per_run(pid) - metadata.update( - metadata_run, - ) # TODO: Not correct for multiple runs + metadata.update(metadata_run) + logger.debug(f"Retrieved metadata with {len(metadata)} entries") return metadata def _get_metadata_per_run(self, pid: str) -> dict: @@ -91,26 +93,26 @@ def _get_metadata_per_run(self, pid: str) -> dict: headers2["Authorization"] = f"Bearer {self.token}" try: + logger.debug(f"Attempting to fetch metadata with new URL format for PID: {pid}") dataset_response = requests.get( self._create_new_dataset_url(pid), headers=headers2, timeout=10, ) dataset_response.raise_for_status() - # Check if response is an empty object because wrong url for older implementation + if not dataset_response.content: + logger.debug("Empty response, trying old URL format") dataset_response = requests.get( self._create_old_dataset_url(pid), headers=headers2, timeout=10, ) - # If the dataset request is successful, return the retrieved metadata - # as a JSON object return dataset_response.json() + except requests.exceptions.RequestException as exception: - # If the request fails, raise warning - print(warnings.warn(f"Failed to retrieve metadata for PID {pid}: {str(exception)}")) - return {} # Return an empty dictionary for this run + logger.warning(f"Failed to retrieve metadata for PID {pid}: {str(exception)}") + return {} def _create_old_dataset_url(self, pid: str) -> str: return "{burl}/{url}/%2F{npid}".format( diff --git a/tests/loader/flash/test_flash_metadata.py b/tests/loader/flash/test_flash_metadata.py index 5b30b0da..84ec3900 100644 --- a/tests/loader/flash/test_flash_metadata.py +++ b/tests/loader/flash/test_flash_metadata.py @@ -16,8 +16,8 @@ def mock_requests(requests_mock) -> None: # Test cases for MetadataRetriever def test_get_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { - "scicat_url": "https://example.com", - "scicat_token": "fake_token", + "archiver_url": "https://example.com", + "token": "fake_token", } retriever = MetadataRetriever(metadata_config) metadata = retriever.get_metadata("11013410", ["43878"]) @@ -27,8 +27,8 @@ def test_get_metadata(mock_requests: None) -> None: # noqa: ARG001 def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { - "scicat_url": "https://example.com", - "scicat_token": "fake_token", + "archiver_url": "https://example.com", + "token": "fake_token", } retriever = MetadataRetriever(metadata_config) existing_metadata = {"existing": "metadata"} @@ -39,8 +39,8 @@ def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # no def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { - "scicat_url": "https://example.com", - "scicat_token": "fake_token", + "archiver_url": "https://example.com", + "token": "fake_token", } retriever = MetadataRetriever(metadata_config) metadata = retriever._get_metadata_per_run("11013410/43878") @@ -50,8 +50,8 @@ def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 def test_create_dataset_url_by_PID() -> None: metadata_config = { - "scicat_url": "https://example.com", - "scicat_token": "fake_token", + "archiver_url": "https://example.com", + "token": "fake_token", } retriever = MetadataRetriever(metadata_config) # Assuming the dataset follows the new format From e2dfcc5efaff7991fa7ff783802c6d77f94dbc76 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:43:01 +0100 Subject: [PATCH 285/300] fix lint errors --- src/sed/loader/flash/buffer_handler.py | 3 +-- src/sed/loader/flash/dataframe.py | 6 +++--- src/sed/loader/flash/metadata.py | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index b7cab37e..d27ae27f 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -234,7 +234,6 @@ def _save_buffer_file(self, paths: dict[str, Path]) -> None: logger.debug(f"Saving timed buffer with shape: {timed_df.shape}") timed_df.to_parquet(paths["timed"]) - logger.debug(f"Processed {paths['raw'].stem} in {time.time() - start_time:.2f}s") def _save_buffer_files(self, force_recreate: bool, debug: bool) -> None: @@ -308,6 +307,7 @@ def _get_dataframes(self) -> None: config=self._config, ) self.metadata.update(meta) + def process_and_load_dataframe( self, h5_paths: list[Path], @@ -358,4 +358,3 @@ def process_and_load_dataframe( self._get_dataframes() return self.df["electron"], self.df["timed"] - diff --git a/src/sed/loader/flash/dataframe.py b/src/sed/loader/flash/dataframe.py index b8a5bfcc..f50abe10 100644 --- a/src/sed/loader/flash/dataframe.py +++ b/src/sed/loader/flash/dataframe.py @@ -299,12 +299,12 @@ def df(self) -> pd.DataFrame: """ logger.debug("Creating combined DataFrame") self.validate_channel_keys() - + df = pd.concat((self.df_electron, self.df_pulse, self.df_train), axis=1).sort_index() logger.debug(f"Created DataFrame with shape: {df.shape}") - + # Filter negative pulse values df = df[df.index.get_level_values("pulseId") >= 0] logger.debug(f"Filtered DataFrame shape: {df.shape}") - + return df diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index 2317b006..f5162850 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -63,7 +63,7 @@ def get_metadata( Exception: If the request to retrieve metadata fails. """ logger.debug(f"Fetching metadata for beamtime {beamtime_id}, runs: {runs}") - + if metadata is None: metadata = {} @@ -100,7 +100,7 @@ def _get_metadata_per_run(self, pid: str) -> dict: timeout=10, ) dataset_response.raise_for_status() - + if not dataset_response.content: logger.debug("Empty response, trying old URL format") dataset_response = requests.get( @@ -109,7 +109,7 @@ def _get_metadata_per_run(self, pid: str) -> dict: timeout=10, ) return dataset_response.json() - + except requests.exceptions.RequestException as exception: logger.warning(f"Failed to retrieve metadata for PID {pid}: {str(exception)}") return {} From 24df1f0ecaf9fa6fca4b47b933a84495a63f2f89 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:50:47 +0100 Subject: [PATCH 286/300] remove repitition --- src/sed/loader/flash/buffer_handler.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/sed/loader/flash/buffer_handler.py b/src/sed/loader/flash/buffer_handler.py index d27ae27f..d56de29f 100644 --- a/src/sed/loader/flash/buffer_handler.py +++ b/src/sed/loader/flash/buffer_handler.py @@ -185,30 +185,6 @@ def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: # Take only first electron per event return df_timed.loc[:, :, 0] - def _create_timed_dataframe(self, df: dd.DataFrame) -> dd.DataFrame: - """Creates the timed dataframe, optionally filtering by electron events. - - Args: - df (dd.DataFrame): The input dataframe containing all data - - Returns: - dd.DataFrame: The timed dataframe - """ - # Get channels that should be in timed dataframe - timed_channels = self.fill_channels - - if self.filter_timed_by_electron: - # Get electron channels to use for filtering - electron_channels = get_channels(self._config, "per_electron") - # Filter rows where electron data exists - df_timed = df.dropna(subset=electron_channels)[timed_channels] - else: - # Take all timed data rows without filtering - df_timed = df[timed_channels] - - # Take only first electron per event - return df_timed.loc[:, :, 0] - def _save_buffer_file(self, paths: dict[str, Path]) -> None: """Creates the electron and timed buffer files from the raw H5 file.""" logger.debug(f"Processing file: {paths['raw'].stem}") From 5eba2e3dc2a858ce4fa5ee4c066dffe44897ecd0 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sat, 11 Jan 2025 00:51:29 +0100 Subject: [PATCH 287/300] remove warnings --- src/sed/loader/flash/metadata.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index f5162850..293ad2cc 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -4,8 +4,6 @@ """ from __future__ import annotations -import warnings - import requests from sed.core.logging import setup_logging From ccd5a8cc213066ecc13bdecfb576f82da59b97fc Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 12 Jan 2025 17:28:26 +0100 Subject: [PATCH 288/300] add documentation about feature --- src/sed/loader/flash/loader.py | 8 ++++---- tutorial/4_hextof_workflow.ipynb | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/sed/loader/flash/loader.py b/src/sed/loader/flash/loader.py index 40106431..b1d4ff81 100644 --- a/src/sed/loader/flash/loader.py +++ b/src/sed/loader/flash/loader.py @@ -303,7 +303,6 @@ def read_dataframe( ftype: str = "h5", metadata: dict = {}, collect_metadata: bool = False, - filter_timed_by_electron: bool = True, **kwds, ) -> tuple[dd.DataFrame, dd.DataFrame, dict]: """ @@ -320,9 +319,6 @@ def read_dataframe( ftype (str, optional): The file extension type. Defaults to "h5". metadata (dict, optional): Additional metadata. Defaults to None. collect_metadata (bool, optional): Whether to collect metadata. Defaults to False. - filter_timed_by_electron (bool, optional): When True, the timed dataframe will only - contain data points where valid electron events were detected. When False, all - timed data points are included regardless of electron detection. Defaults to True. Keyword Args: detector (str, optional): The detector to use. Defaults to "". @@ -334,6 +330,9 @@ def read_dataframe( remove_invalid_files (bool, optional): Whether to exclude invalid files. Defaults to False. scicat_token (str, optional): The scicat token to use for fetching metadata. + filter_timed_by_electron (bool, optional): When True, the timed dataframe will only + contain data points where valid electron events were detected. When False, all + timed data points are included regardless of electron detection. Defaults to True. Returns: tuple[dd.DataFrame, dd.DataFrame, dict]: A tuple containing the concatenated DataFrame @@ -349,6 +348,7 @@ def read_dataframe( debug = kwds.pop("debug", False) remove_invalid_files = kwds.pop("remove_invalid_files", False) scicat_token = kwds.pop("scicat_token", None) + filter_timed_by_electron = kwds.pop("filter_timed_by_electron", True) if len(kwds) > 0: raise ValueError(f"Unexpected keyword arguments: {kwds.keys()}") diff --git a/tutorial/4_hextof_workflow.ipynb b/tutorial/4_hextof_workflow.ipynb index 51e22751..52dcba7f 100644 --- a/tutorial/4_hextof_workflow.ipynb +++ b/tutorial/4_hextof_workflow.ipynb @@ -107,6 +107,13 @@ "assert config_file.exists()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The path to the processed folder can also be defined as a keyword argument later." + ] + }, { "cell_type": "code", "execution_count": null, @@ -162,7 +169,19 @@ "metadata": {}, "source": [ "### Generate the Processor instance\n", - "this cell generates an instance of the `SedProcessor` class. It will be our workhorse for the entire workflow." + "this cell generates an instance of the `SedProcessor` class. It will be our workhorse for the entire workflow.\n", + "\n", + "#### Important note\n", + "The following extra arguments are available for FlashLoader. None of which are necessary to give but helpful to know.\n", + "- **force_recreate**: Probably the most useful. In case the config is changed, this allows to reduce the raw h5 files to the the intermediate parquet format again. Otherwise, the schema between the saved dataframe and config differs.\n", + "- **debug**: Setting this runs the reduction process in serial, so the errors are easier to find.\n", + "- **remove_invalid_files**: Sometimes some critical channels defined in the config are missing in some raw files. Setting this will make sure to ignore such files.\n", + "- **filter_timed_by_electron**: Defaults to True. When True, the timed dataframe will only\n", + " contain data points where valid electron events were detected. When False, all\n", + " timed data points are included regardless of electron detection (see https://github.com/OpenCOMPES/sed/issues/307)\n", + "- **processed_dir**: Location to save the reduced parquet files. \n", + "- **scicat_token**: Token from your scicat account.\n", + "- **detector**: '1Q' and '4Q' detector for example. Useful when there are separate raw files for each detector." ] }, { From d66d34ddf1d9bc7c57f9eead655f397a8752cdb4 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 12 Jan 2025 18:20:41 +0100 Subject: [PATCH 289/300] update token handling with env variables --- .cspell/custom-dictionary.txt | 1 + src/sed/core/config_model.py | 2 -- src/sed/loader/flash/loader.py | 15 ++++++++----- src/sed/loader/flash/metadata.py | 38 ++++++++++++++++++++++++-------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 264069be..6391f3a0 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -89,6 +89,7 @@ dictionarized dictmerge DLDAUX DOOCS +dotenv dpkg dropna dset diff --git a/src/sed/core/config_model.py b/src/sed/core/config_model.py index 880e5864..6379b639 100644 --- a/src/sed/core/config_model.py +++ b/src/sed/core/config_model.py @@ -14,7 +14,6 @@ from pydantic import HttpUrl from pydantic import NewPath from pydantic import PositiveInt -from pydantic import SecretStr from sed.loader.loader_interface import get_names_of_all_loaders @@ -323,7 +322,6 @@ class MetadataModel(BaseModel): model_config = ConfigDict(extra="forbid") archiver_url: Optional[HttpUrl] = None - token: Optional[SecretStr] = None epics_pvs: Optional[Sequence[str]] = None fa_in_channel: Optional[str] = None fa_hor_channel: Optional[str] = None diff --git a/src/sed/loader/flash/loader.py b/src/sed/loader/flash/loader.py index b1d4ff81..c2cf79b9 100644 --- a/src/sed/loader/flash/loader.py +++ b/src/sed/loader/flash/loader.py @@ -207,14 +207,14 @@ def get_files_from_run_id( # type: ignore[override] # Return the list of found files return [str(file.resolve()) for file in files] - def parse_metadata(self, scicat_token: str = None) -> dict: + def parse_metadata(self, token: str = None) -> dict: """Uses the MetadataRetriever class to fetch metadata from scicat for each run. Returns: dict: Metadata dictionary - scicat_token (str, optional):: The scicat token to use for fetching metadata + token (str, optional):: The scicat token to use for fetching metadata """ - metadata_retriever = MetadataRetriever(self._config["metadata"], scicat_token) + metadata_retriever = MetadataRetriever(self._config["metadata"], token) metadata = metadata_retriever.get_metadata( beamtime_id=self._config["core"]["beamtime_id"], runs=self.runs, @@ -329,7 +329,9 @@ def read_dataframe( debug (bool, optional): Whether to run buffer creation in serial. Defaults to False. remove_invalid_files (bool, optional): Whether to exclude invalid files. Defaults to False. - scicat_token (str, optional): The scicat token to use for fetching metadata. + token (str, optional): The scicat token to use for fetching metadata. If provided, + will be saved to .env file for future use. If not provided, will check environment + variables when collect_metadata is True. filter_timed_by_electron (bool, optional): When True, the timed dataframe will only contain data points where valid electron events were detected. When False, all timed data points are included regardless of electron detection. Defaults to True. @@ -341,13 +343,14 @@ def read_dataframe( Raises: ValueError: If neither 'runs' nor 'files'/'raw_dir' is provided. FileNotFoundError: If the conversion fails for some files or no data is available. + ValueError: If collect_metadata is True and no token is available. """ detector = kwds.pop("detector", "") force_recreate = kwds.pop("force_recreate", False) processed_dir = kwds.pop("processed_dir", None) debug = kwds.pop("debug", False) remove_invalid_files = kwds.pop("remove_invalid_files", False) - scicat_token = kwds.pop("scicat_token", None) + token = kwds.pop("token", None) filter_timed_by_electron = kwds.pop("filter_timed_by_electron", True) if len(kwds) > 0: @@ -401,7 +404,7 @@ def read_dataframe( if self.instrument == "wespe": df, df_timed = wespe_convert(df, df_timed) - self.metadata.update(self.parse_metadata(scicat_token) if collect_metadata else {}) + self.metadata.update(self.parse_metadata(token) if collect_metadata else {}) self.metadata.update(bh.metadata) print(f"loading complete in {time.time() - t0: .2f} s") diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index 293ad2cc..d7279e43 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -4,7 +4,13 @@ """ from __future__ import annotations +import os +from pathlib import Path + import requests +from dotenv import load_dotenv +from dotenv import set_key + from sed.core.logging import setup_logging logger = setup_logging("flash_metadata_retriever") @@ -21,23 +27,37 @@ def __init__(self, metadata_config: dict, token: str = None) -> None: Initializes the MetadataRetriever class. Args: - metadata_config (dict): Takes a dict containing - at least url, and optionally token for the scicat instance. - token (str, optional): The token to use for fetching metadata. + metadata_config (dict): Takes a dict containing at least url for the scicat instance. + token (str, optional): The token to use for fetching metadata. If provided, + will be saved to .env file for future use. """ - self.token = metadata_config.get("token", None) + # Token handling if token: - self.token = token - self.url = metadata_config.get("archiver_url", None) + # Save token to .env file in user's home directory + env_path = Path.home() / ".sed" / ".env" + env_path.parent.mkdir(parents=True, exist_ok=True) + set_key(str(env_path), "SCICAT_TOKEN", token) + else: + # Try to load token from config or environment + self.token = metadata_config.get("token") + if not self.token: + load_dotenv(Path.home() / ".sed" / ".env") + self.token = os.getenv("SCICAT_TOKEN") + + if not self.token: + raise ValueError( + "Token is required for metadata collection. Either provide a token " + "parameter or set the SCICAT_TOKEN environment variable.", + ) - if not self.token or not self.url: - raise ValueError("No URL or token provided for fetching metadata from scicat.") + self.url = metadata_config.get("archiver_url") + if not self.url: + raise ValueError("No URL provided for fetching metadata from scicat.") self.headers = { "Content-Type": "application/json", "Accept": "application/json", } - self.token = metadata_config["token"] def get_metadata( self, From 9a7a8b03d2e3e0aa5f092df8dd234f21843c6917 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 12 Jan 2025 18:31:55 +0100 Subject: [PATCH 290/300] update test --- tests/loader/flash/test_flash_metadata.py | 56 ++++++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/tests/loader/flash/test_flash_metadata.py b/tests/loader/flash/test_flash_metadata.py index 84ec3900..2f30537c 100644 --- a/tests/loader/flash/test_flash_metadata.py +++ b/tests/loader/flash/test_flash_metadata.py @@ -1,6 +1,8 @@ """Tests for FlashLoader metadata functionality""" from __future__ import annotations +from pathlib import Path + import pytest from sed.loader.flash.metadata import MetadataRetriever @@ -13,11 +15,38 @@ def mock_requests(requests_mock) -> None: requests_mock.get(dataset_url, json={"fake": "data"}, status_code=200) -# Test cases for MetadataRetriever -def test_get_metadata(mock_requests: None) -> None: # noqa: ARG001 +@pytest.fixture +def mock_env_token(monkeypatch, tmp_path) -> None: + # Create a temporary .env file + env_path = tmp_path / ".env" + env_path.write_text("SCICAT_TOKEN=env_test_token") + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + +def test_get_metadata_with_explicit_token(mock_requests: None) -> None: # noqa: ARG001 + metadata_config = { + "archiver_url": "https://example.com", + } + retriever = MetadataRetriever(metadata_config, token="explicit_test_token") + metadata = retriever.get_metadata("11013410", ["43878"]) + assert isinstance(metadata, dict) + assert metadata == {"fake": "data"} + + +def test_get_metadata_with_config_token(mock_requests: None) -> None: # noqa: ARG001 + metadata_config = { + "archiver_url": "https://example.com", + "token": "config_test_token", + } + retriever = MetadataRetriever(metadata_config) + metadata = retriever.get_metadata("11013410", ["43878"]) + assert isinstance(metadata, dict) + assert metadata == {"fake": "data"} + + +def test_get_metadata_with_env_token(mock_requests: None, mock_env_token: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", - "token": "fake_token", } retriever = MetadataRetriever(metadata_config) metadata = retriever.get_metadata("11013410", ["43878"]) @@ -25,10 +54,24 @@ def test_get_metadata(mock_requests: None) -> None: # noqa: ARG001 assert metadata == {"fake": "data"} +def test_get_metadata_no_token() -> None: + metadata_config = { + "archiver_url": "https://example.com", + } + with pytest.raises(ValueError, match="Token is required for metadata collection"): + MetadataRetriever(metadata_config) + + +def test_get_metadata_no_url() -> None: + metadata_config: dict = {} + with pytest.raises(ValueError, match="No URL provided for fetching metadata"): + MetadataRetriever(metadata_config, token="test_token") + + def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", - "token": "fake_token", + "token": "test_token", } retriever = MetadataRetriever(metadata_config) existing_metadata = {"existing": "metadata"} @@ -40,7 +83,7 @@ def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # no def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", - "token": "fake_token", + "token": "test_token", } retriever = MetadataRetriever(metadata_config) metadata = retriever._get_metadata_per_run("11013410/43878") @@ -51,10 +94,9 @@ def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 def test_create_dataset_url_by_PID() -> None: metadata_config = { "archiver_url": "https://example.com", - "token": "fake_token", + "token": "test_token", } retriever = MetadataRetriever(metadata_config) - # Assuming the dataset follows the new format pid = "11013410/43878" url = retriever._create_new_dataset_url(pid) expected_url = "https://example.com/Datasets/11013410%2F43878" From 9d22be7234d368e64354e36d81ce8e1b411bb058 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 12 Jan 2025 18:47:49 +0100 Subject: [PATCH 291/300] add the dotenv package --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6a852d9d..168d279d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "joblib>=1.2.0", "pyarrow>=14.0.1,<17.0", "pydantic>=2.8.2", + "python-dotenv>=1.0.1", ] [project.urls] From 80ef9c165b08d2aecb876e663279232097ffafe2 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Sun, 12 Jan 2025 23:47:57 +0100 Subject: [PATCH 292/300] read write env variables without extra package, tests added --- pyproject.toml | 1 - src/sed/core/config.py | 51 +++++++++++++++++++++++ src/sed/loader/flash/metadata.py | 20 +++------ tests/loader/flash/test_flash_metadata.py | 46 +++++++++----------- tests/test_config.py | 47 +++++++++++++++++++++ 5 files changed, 123 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 168d279d..6a852d9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ dependencies = [ "joblib>=1.2.0", "pyarrow>=14.0.1,<17.0", "pydantic>=2.8.2", - "python-dotenv>=1.0.1", ] [project.urls] diff --git a/src/sed/core/config.py b/src/sed/core/config.py index 647b1c33..a870e536 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -242,3 +242,54 @@ def complete_dictionary(dictionary: dict, base_dictionary: dict) -> dict: dictionary[k] = v return dictionary + + +def read_env_var(var_name: str) -> str | None: + """Read an environment variable from the .env file in the user config directory. + + Args: + var_name (str): Name of the environment variable to read + + Returns: + str | None: Value of the environment variable or None if not found + """ + env_path = USER_CONFIG_PATH / ".env" + if not env_path.exists(): + logger.debug(f"Environment variable {var_name} not found in .env file") + return None + + with open(env_path) as f: + for line in f: + if line.startswith(f"{var_name}="): + return line.strip().split("=", 1)[1] + logger.debug(f"Environment variable {var_name} not found in .env file") + return None + + +def save_env_var(var_name: str, value: str) -> None: + """Save an environment variable to the .env file in the user config directory. + If the file exists, preserves other variables. If not, creates a new file. + + Args: + var_name (str): Name of the environment variable to save + value (str): Value to save for the environment variable + """ + env_path = USER_CONFIG_PATH / ".env" + env_content = {} + + # Read existing variables if file exists + if env_path.exists(): + with open(env_path) as f: + for line in f: + if "=" in line: + key, val = line.strip().split("=", 1) + env_content[key] = val + + # Update or add new variable + env_content[var_name] = value + + # Write all variables back to file + with open(env_path, "w") as f: + for key, val in env_content.items(): + f.write(f"{key}={val}\n") + logger.debug(f"Environment variable {var_name} saved to .env file") diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index d7279e43..9703907b 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -4,13 +4,10 @@ """ from __future__ import annotations -import os -from pathlib import Path - import requests -from dotenv import load_dotenv -from dotenv import set_key +from sed.core.config import read_env_var +from sed.core.config import save_env_var from sed.core.logging import setup_logging logger = setup_logging("flash_metadata_retriever") @@ -33,16 +30,11 @@ def __init__(self, metadata_config: dict, token: str = None) -> None: """ # Token handling if token: - # Save token to .env file in user's home directory - env_path = Path.home() / ".sed" / ".env" - env_path.parent.mkdir(parents=True, exist_ok=True) - set_key(str(env_path), "SCICAT_TOKEN", token) + self.token = token + save_env_var("SCICAT_TOKEN", self.token) else: - # Try to load token from config or environment - self.token = metadata_config.get("token") - if not self.token: - load_dotenv(Path.home() / ".sed" / ".env") - self.token = os.getenv("SCICAT_TOKEN") + # Try to load token from config or .env file + self.token = read_env_var("SCICAT_TOKEN") if not self.token: raise ValueError( diff --git a/tests/loader/flash/test_flash_metadata.py b/tests/loader/flash/test_flash_metadata.py index 2f30537c..165a6e8a 100644 --- a/tests/loader/flash/test_flash_metadata.py +++ b/tests/loader/flash/test_flash_metadata.py @@ -1,12 +1,17 @@ """Tests for FlashLoader metadata functionality""" from __future__ import annotations -from pathlib import Path +import os import pytest +from sed.core.config import read_env_var +from sed.core.config import save_env_var +from sed.core.config import USER_CONFIG_PATH from sed.loader.flash.metadata import MetadataRetriever +ENV_PATH = USER_CONFIG_PATH / ".env" + @pytest.fixture def mock_requests(requests_mock) -> None: @@ -15,14 +20,6 @@ def mock_requests(requests_mock) -> None: requests_mock.get(dataset_url, json={"fake": "data"}, status_code=200) -@pytest.fixture -def mock_env_token(monkeypatch, tmp_path) -> None: - # Create a temporary .env file - env_path = tmp_path / ".env" - env_path.write_text("SCICAT_TOKEN=env_test_token") - monkeypatch.setattr(Path, "home", lambda: tmp_path) - - def test_get_metadata_with_explicit_token(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", @@ -31,20 +28,13 @@ def test_get_metadata_with_explicit_token(mock_requests: None) -> None: # noqa: metadata = retriever.get_metadata("11013410", ["43878"]) assert isinstance(metadata, dict) assert metadata == {"fake": "data"} + assert ENV_PATH.exists() + assert read_env_var("SCICAT_TOKEN") == "explicit_test_token" + os.remove(ENV_PATH) -def test_get_metadata_with_config_token(mock_requests: None) -> None: # noqa: ARG001 - metadata_config = { - "archiver_url": "https://example.com", - "token": "config_test_token", - } - retriever = MetadataRetriever(metadata_config) - metadata = retriever.get_metadata("11013410", ["43878"]) - assert isinstance(metadata, dict) - assert metadata == {"fake": "data"} - - -def test_get_metadata_with_env_token(mock_requests: None, mock_env_token: None) -> None: # noqa: ARG001 +def test_get_metadata_with_env_token(mock_requests: None) -> None: # noqa: ARG001 + save_env_var("SCICAT_TOKEN", "env_test_token") metadata_config = { "archiver_url": "https://example.com", } @@ -52,6 +42,7 @@ def test_get_metadata_with_env_token(mock_requests: None, mock_env_token: None) metadata = retriever.get_metadata("11013410", ["43878"]) assert isinstance(metadata, dict) assert metadata == {"fake": "data"} + os.remove(ENV_PATH) def test_get_metadata_no_token() -> None: @@ -66,39 +57,40 @@ def test_get_metadata_no_url() -> None: metadata_config: dict = {} with pytest.raises(ValueError, match="No URL provided for fetching metadata"): MetadataRetriever(metadata_config, token="test_token") + os.remove(ENV_PATH) def test_get_metadata_with_existing_metadata(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", - "token": "test_token", } - retriever = MetadataRetriever(metadata_config) + retriever = MetadataRetriever(metadata_config, token="test_token") existing_metadata = {"existing": "metadata"} metadata = retriever.get_metadata("11013410", ["43878"], existing_metadata) assert isinstance(metadata, dict) assert metadata == {"existing": "metadata", "fake": "data"} + os.remove(ENV_PATH) def test_get_metadata_per_run(mock_requests: None) -> None: # noqa: ARG001 metadata_config = { "archiver_url": "https://example.com", - "token": "test_token", } - retriever = MetadataRetriever(metadata_config) + retriever = MetadataRetriever(metadata_config, token="test_token") metadata = retriever._get_metadata_per_run("11013410/43878") assert isinstance(metadata, dict) assert metadata == {"fake": "data"} + os.remove(ENV_PATH) def test_create_dataset_url_by_PID() -> None: metadata_config = { "archiver_url": "https://example.com", - "token": "test_token", } - retriever = MetadataRetriever(metadata_config) + retriever = MetadataRetriever(metadata_config, token="test_token") pid = "11013410/43878" url = retriever._create_new_dataset_url(pid) expected_url = "https://example.com/Datasets/11013410%2F43878" assert isinstance(url, str) assert url == expected_url + os.remove(ENV_PATH) diff --git a/tests/test_config.py b/tests/test_config.py index 7f8a43b8..321b35d6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,7 +13,9 @@ from sed.core.config import complete_dictionary from sed.core.config import load_config from sed.core.config import parse_config +from sed.core.config import read_env_var from sed.core.config import save_config +from sed.core.config import save_env_var test_dir = os.path.dirname(__file__) test_config_dir = Path(f"{test_dir}/data/loader/") @@ -231,3 +233,48 @@ def test_invalid_config_wrong_values(): verify_config=True, ) assert "Invalid value 9999 for gid. Group not found." in str(e.value) + + +def test_env_var_read_write(tmp_path, monkeypatch): + """Test reading and writing environment variables.""" + # Mock USER_CONFIG_PATH to use a temporary directory + monkeypatch.setattr("sed.core.config.USER_CONFIG_PATH", tmp_path) + + # Test writing a new variable + save_env_var("TEST_VAR", "test_value") + assert read_env_var("TEST_VAR") == "test_value" + + # Test writing multiple variables + save_env_var("TEST_VAR2", "test_value2") + assert read_env_var("TEST_VAR") == "test_value" + assert read_env_var("TEST_VAR2") == "test_value2" + + # Test overwriting an existing variable + save_env_var("TEST_VAR", "new_value") + assert read_env_var("TEST_VAR") == "new_value" + assert read_env_var("TEST_VAR2") == "test_value2" # Other variables unchanged + + # Test reading non-existent variable + assert read_env_var("NON_EXISTENT_VAR") is None + + +def test_env_var_read_no_file(tmp_path, monkeypatch): + """Test reading environment variables when .env file doesn't exist.""" + # Mock USER_CONFIG_PATH to use an empty temporary directory + monkeypatch.setattr("sed.core.config.USER_CONFIG_PATH", tmp_path) + + # Test reading from non-existent file + assert read_env_var("TEST_VAR") is None + + +def test_env_var_special_characters(): + """Test reading and writing environment variables with special characters.""" + test_cases = { + "TEST_URL": "http://example.com/path?query=value", + "TEST_PATH": "/path/to/something/with/spaces and special=chars", + "TEST_QUOTES": "value with 'single' and \"double\" quotes", + } + + for var_name, value in test_cases.items(): + save_env_var(var_name, value) + assert read_env_var(var_name) == value From 0adb9615691e47cd1ca67c95298a7ab8a45d90d2 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Mon, 13 Jan 2025 01:02:19 +0100 Subject: [PATCH 293/300] config error --- src/sed/core/config.py | 29 ++++++++++++++++++++++++++--- tests/test_config.py | 10 +++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/sed/core/config.py b/src/sed/core/config.py index 647b1c33..5cf8b2c8 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -11,6 +11,7 @@ import yaml from platformdirs import user_config_path +from pydantic import ValidationError from sed.core.config_model import ConfigModel from sed.core.logging import setup_logging @@ -23,6 +24,17 @@ logger = setup_logging("config") +class ConfigError(Exception): + """Exception raised for errors in the config file.""" + + def __init__(self, message: str): + self.message = message + super().__init__(self.message) + + def __str__(self): + return self.message + + def parse_config( config: dict | str = None, folder_config: dict | str = None, @@ -61,6 +73,7 @@ def parse_config( Raises: TypeError: Raised if the provided file is neither *json* nor *yaml*. FileNotFoundError: Raised if the provided file is not found. + ConfigError: Raised if there is a validation error in the config file. Returns: dict: Loaded and completed config dict, possibly verified by pydantic config model. @@ -146,9 +159,19 @@ def parse_config( if not verify_config: return config_dict - # Run the config through the ConfigModel to ensure it is valid - config_model = ConfigModel(**config_dict) - return config_model.model_dump(exclude_unset=True, exclude_none=True) + + try: + # Run the config through the ConfigModel to ensure it is valid + config_model = ConfigModel(**config_dict) + return config_model.model_dump(exclude_unset=True, exclude_none=True) + except ValidationError as e: + error_msg = ( + "Invalid configuration file detected. The following validation errors were found:\n" + ) + for error in e.errors(): + error_msg += f"\n- {' -> '.join(str(loc) for loc in error['loc'])}: {error['msg']}" + logger.error(error_msg) + raise ConfigError(error_msg) from e def load_config(config_path: str) -> dict: diff --git a/tests/test_config.py b/tests/test_config.py index 7f8a43b8..a6569246 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,9 +8,9 @@ from pathlib import Path import pytest -from pydantic import ValidationError from sed.core.config import complete_dictionary +from sed.core.config import ConfigError from sed.core.config import load_config from sed.core.config import parse_config from sed.core.config import save_config @@ -168,7 +168,7 @@ def test_invalid_config_extra_field(): ) invalid_config = default_config.copy() invalid_config["extra_field"] = "extra_value" - with pytest.raises(ValidationError): + with pytest.raises(ConfigError): parse_config( invalid_config, folder_config={}, @@ -188,7 +188,7 @@ def test_invalid_config_missing_field(): ) invalid_config = default_config.copy() del invalid_config["core"]["loader"] - with pytest.raises(ValidationError): + with pytest.raises(ConfigError): parse_config( folder_config={}, user_config={}, @@ -208,7 +208,7 @@ def test_invalid_config_wrong_values(): ) invalid_config = default_config.copy() invalid_config["core"]["loader"] = "nonexistent" - with pytest.raises(ValidationError) as e: + with pytest.raises(ConfigError) as e: parse_config( folder_config={}, user_config={}, @@ -222,7 +222,7 @@ def test_invalid_config_wrong_values(): invalid_config["core"]["copy_tool"]["source"] = "./" invalid_config["core"]["copy_tool"]["dest"] = "./" invalid_config["core"]["copy_tool"]["gid"] = 9999 - with pytest.raises(ValidationError) as e: + with pytest.raises(ConfigError) as e: parse_config( folder_config={}, user_config={}, From 5084bb7b509393d2364714b3020911c973c0ce45 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 14 Jan 2025 12:30:35 +0100 Subject: [PATCH 294/300] use value error --- src/sed/core/config.py | 15 ++------------- tests/test_config.py | 9 ++++----- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/sed/core/config.py b/src/sed/core/config.py index 5cf8b2c8..d686a891 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -24,17 +24,6 @@ logger = setup_logging("config") -class ConfigError(Exception): - """Exception raised for errors in the config file.""" - - def __init__(self, message: str): - self.message = message - super().__init__(self.message) - - def __str__(self): - return self.message - - def parse_config( config: dict | str = None, folder_config: dict | str = None, @@ -73,7 +62,7 @@ def parse_config( Raises: TypeError: Raised if the provided file is neither *json* nor *yaml*. FileNotFoundError: Raised if the provided file is not found. - ConfigError: Raised if there is a validation error in the config file. + ValueError: Raised if there is a validation error in the config file. Returns: dict: Loaded and completed config dict, possibly verified by pydantic config model. @@ -171,7 +160,7 @@ def parse_config( for error in e.errors(): error_msg += f"\n- {' -> '.join(str(loc) for loc in error['loc'])}: {error['msg']}" logger.error(error_msg) - raise ConfigError(error_msg) from e + raise ValueError(error_msg) from e def load_config(config_path: str) -> dict: diff --git a/tests/test_config.py b/tests/test_config.py index a6569246..8545f64b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,7 +10,6 @@ import pytest from sed.core.config import complete_dictionary -from sed.core.config import ConfigError from sed.core.config import load_config from sed.core.config import parse_config from sed.core.config import save_config @@ -168,7 +167,7 @@ def test_invalid_config_extra_field(): ) invalid_config = default_config.copy() invalid_config["extra_field"] = "extra_value" - with pytest.raises(ConfigError): + with pytest.raises(ValueError): parse_config( invalid_config, folder_config={}, @@ -188,7 +187,7 @@ def test_invalid_config_missing_field(): ) invalid_config = default_config.copy() del invalid_config["core"]["loader"] - with pytest.raises(ConfigError): + with pytest.raises(ValueError): parse_config( folder_config={}, user_config={}, @@ -208,7 +207,7 @@ def test_invalid_config_wrong_values(): ) invalid_config = default_config.copy() invalid_config["core"]["loader"] = "nonexistent" - with pytest.raises(ConfigError) as e: + with pytest.raises(ValueError) as e: parse_config( folder_config={}, user_config={}, @@ -222,7 +221,7 @@ def test_invalid_config_wrong_values(): invalid_config["core"]["copy_tool"]["source"] = "./" invalid_config["core"]["copy_tool"]["dest"] = "./" invalid_config["core"]["copy_tool"]["gid"] = 9999 - with pytest.raises(ConfigError) as e: + with pytest.raises(ValueError) as e: parse_config( folder_config={}, user_config={}, From 9b13c991ecc9a481c3d579fb928dadba2dbc6faf Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 14 Jan 2025 14:02:59 +0100 Subject: [PATCH 295/300] search for .env in cwd and os env variables --- src/sed/core/config.py | 64 ++++++++++++++++++++++----------- tests/test_config.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 20 deletions(-) diff --git a/src/sed/core/config.py b/src/sed/core/config.py index a870e536..328353e4 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -244,8 +244,31 @@ def complete_dictionary(dictionary: dict, base_dictionary: dict) -> dict: return dictionary +def _parse_env_file(file_path: Path) -> dict: + """Helper function to parse a .env file into a dictionary. + + Args: + file_path (Path): Path to the .env file + + Returns: + dict: Dictionary of environment variables from the file + """ + env_content = {} + if file_path.exists(): + with open(file_path) as f: + for line in f: + line = line.strip() + if line and "=" in line: + key, val = line.split("=", 1) + env_content[key.strip()] = val.strip() + return env_content + + def read_env_var(var_name: str) -> str | None: - """Read an environment variable from the .env file in the user config directory. + """Read an environment variable from multiple locations in order: + 1. OS environment variables + 2. .env file in current directory + 3. .env file in user config directory Args: var_name (str): Name of the environment variable to read @@ -253,16 +276,25 @@ def read_env_var(var_name: str) -> str | None: Returns: str | None: Value of the environment variable or None if not found """ - env_path = USER_CONFIG_PATH / ".env" - if not env_path.exists(): - logger.debug(f"Environment variable {var_name} not found in .env file") - return None - - with open(env_path) as f: - for line in f: - if line.startswith(f"{var_name}="): - return line.strip().split("=", 1)[1] - logger.debug(f"Environment variable {var_name} not found in .env file") + # First check OS environment variables + value = os.getenv(var_name) + if value is not None: + logger.debug(f"Found {var_name} in OS environment variables") + return value + + # Then check .env in current directory + local_vars = _parse_env_file(Path(".env")) + if var_name in local_vars: + logger.debug(f"Found {var_name} in ./.env file") + return local_vars[var_name] + + # Finally check .env in user config directory + user_vars = _parse_env_file(USER_CONFIG_PATH / ".env") + if var_name in user_vars: + logger.debug(f"Found {var_name} in user config .env file") + return user_vars[var_name] + + logger.debug(f"Environment variable {var_name} not found in any location") return None @@ -275,15 +307,7 @@ def save_env_var(var_name: str, value: str) -> None: value (str): Value to save for the environment variable """ env_path = USER_CONFIG_PATH / ".env" - env_content = {} - - # Read existing variables if file exists - if env_path.exists(): - with open(env_path) as f: - for line in f: - if "=" in line: - key, val = line.strip().split("=", 1) - env_content[key] = val + env_content = _parse_env_file(env_path) # Update or add new variable env_content[var_name] = value diff --git a/tests/test_config.py b/tests/test_config.py index 321b35d6..75d780c6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,6 +16,7 @@ from sed.core.config import read_env_var from sed.core.config import save_config from sed.core.config import save_env_var +from sed.core.config import USER_CONFIG_PATH test_dir = os.path.dirname(__file__) test_config_dir = Path(f"{test_dir}/data/loader/") @@ -278,3 +279,83 @@ def test_env_var_special_characters(): for var_name, value in test_cases.items(): save_env_var(var_name, value) assert read_env_var(var_name) == value + + +@pytest.fixture +def cleanup_env_files(): + """Cleanup any .env files before and after tests""" + # Clean up any existing .env files + for path in [Path(".env"), USER_CONFIG_PATH / ".env"]: + if path.exists(): + path.unlink() + + yield + + # Clean up after tests + for path in [Path(".env"), USER_CONFIG_PATH / ".env"]: + if path.exists(): + path.unlink() + + +def test_env_var_precedence(cleanup_env_files): # noqa: ARG001 + """Test that environment variables are read in correct order of precedence""" + # Set up test values in different locations + os.environ["TEST_VAR"] = "os_value" + + with open(".env", "w") as f: + f.write("TEST_VAR=local_value\n") + + save_env_var("TEST_VAR", "user_value") # Saves to USER_CONFIG_PATH + + # Should get OS value first + assert read_env_var("TEST_VAR") == "os_value" + + # Remove from OS env and should get local value + del os.environ["TEST_VAR"] + assert read_env_var("TEST_VAR") == "local_value" + + # Remove local .env and should get user config value + Path(".env").unlink() + assert read_env_var("TEST_VAR") == "user_value" + + # Remove user config and should get None + (USER_CONFIG_PATH / ".env").unlink() + assert read_env_var("TEST_VAR") is None + + +def test_env_var_save_and_load(cleanup_env_files): # noqa: ARG001 + """Test saving and loading environment variables""" + # Save a variable + save_env_var("TEST_VAR", "test_value") + + # Should be able to read it back + assert read_env_var("TEST_VAR") == "test_value" + + # Save another variable - should preserve existing ones + save_env_var("OTHER_VAR", "other_value") + assert read_env_var("TEST_VAR") == "test_value" + assert read_env_var("OTHER_VAR") == "other_value" + + +def test_env_var_not_found(cleanup_env_files): # noqa: ARG001 + """Test behavior when environment variable is not found""" + assert read_env_var("NONEXISTENT_VAR") is None + + +def test_env_file_format(cleanup_env_files): # noqa: ARG001 + """Test that .env file parsing handles different formats correctly""" + with open(".env", "w") as f: + f.write( + """ + TEST_VAR=value1 + SPACES_VAR = value2 + EMPTY_VAR= + #COMMENT=value3 + INVALID_LINE + """, + ) + + assert read_env_var("TEST_VAR") == "value1" + assert read_env_var("SPACES_VAR") == "value2" + assert read_env_var("EMPTY_VAR") == "" + assert read_env_var("COMMENT") is None From 25a935beb8bf9eb48ce71ea392df94a1f621ba2a Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Tue, 14 Jan 2025 14:08:21 +0100 Subject: [PATCH 296/300] bring back comments --- .cspell/custom-dictionary.txt | 1 - src/sed/loader/flash/metadata.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 6391f3a0..264069be 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -89,7 +89,6 @@ dictionarized dictmerge DLDAUX DOOCS -dotenv dpkg dropna dset diff --git a/src/sed/loader/flash/metadata.py b/src/sed/loader/flash/metadata.py index 9703907b..578fa9fd 100644 --- a/src/sed/loader/flash/metadata.py +++ b/src/sed/loader/flash/metadata.py @@ -81,7 +81,7 @@ def get_metadata( pid = f"{beamtime_id}/{run}" logger.debug(f"Retrieving metadata for PID: {pid}") metadata_run = self._get_metadata_per_run(pid) - metadata.update(metadata_run) + metadata.update(metadata_run) # TODO: Not correct for multiple runs logger.debug(f"Retrieved metadata with {len(metadata)} entries") return metadata @@ -111,6 +111,7 @@ def _get_metadata_per_run(self, pid: str) -> dict: ) dataset_response.raise_for_status() + # Check if response is an empty object because wrong url for older implementation if not dataset_response.content: logger.debug("Empty response, trying old URL format") dataset_response = requests.get( @@ -118,11 +119,13 @@ def _get_metadata_per_run(self, pid: str) -> dict: headers=headers2, timeout=10, ) + # If the dataset request is successful, return the retrieved metadata + # as a JSON object return dataset_response.json() except requests.exceptions.RequestException as exception: logger.warning(f"Failed to retrieve metadata for PID {pid}: {str(exception)}") - return {} + return {} # Return an empty dictionary for this run def _create_old_dataset_url(self, pid: str) -> str: return "{burl}/{url}/%2F{npid}".format( From b3ad646965a6800a24f4da6b09e608b7b0e168d4 Mon Sep 17 00:00:00 2001 From: Zain Sohail Date: Thu, 16 Jan 2025 22:07:30 +0100 Subject: [PATCH 297/300] update tests --- .cspell/custom-dictionary.txt | 1 + src/sed/core/config.py | 3 +- tests/test_config.py | 78 +++++++++++++++++------------------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index 264069be..5123e47d 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -79,6 +79,7 @@ datestring ddir delaxes delayeds +delenv Desy Deutsches dfield diff --git a/src/sed/core/config.py b/src/sed/core/config.py index 686ee5b3..c12f9fa1 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -19,6 +19,7 @@ package_dir = os.path.dirname(find_spec("sed").origin) USER_CONFIG_PATH = user_config_path(appname="sed", appauthor="OpenCOMPES", ensure_exists=True) +ENV_DIR = Path(".env") # Configure logging logger = setup_logging("config") @@ -295,7 +296,7 @@ def read_env_var(var_name: str) -> str | None: return value # Then check .env in current directory - local_vars = _parse_env_file(Path(".env")) + local_vars = _parse_env_file(ENV_DIR) if var_name in local_vars: logger.debug(f"Found {var_name} in ./.env file") return local_vars[var_name] diff --git a/tests/test_config.py b/tests/test_config.py index 3760e0cd..fb04fb65 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,7 +15,6 @@ from sed.core.config import read_env_var from sed.core.config import save_config from sed.core.config import save_env_var -from sed.core.config import USER_CONFIG_PATH test_dir = os.path.dirname(__file__) test_config_dir = Path(f"{test_dir}/data/loader/") @@ -235,11 +234,15 @@ def test_invalid_config_wrong_values(): assert "Invalid value 9999 for gid. Group not found." in str(e.value) -def test_env_var_read_write(tmp_path, monkeypatch): - """Test reading and writing environment variables.""" - # Mock USER_CONFIG_PATH to use a temporary directory +@pytest.fixture +def mock_env_file(tmp_path, monkeypatch): + """Mock the .env file for testing""" monkeypatch.setattr("sed.core.config.USER_CONFIG_PATH", tmp_path) + yield tmp_path + +def test_env_var_read_write(mock_env_file): # noqa: ARG001 + """Test reading and writing environment variables.""" # Test writing a new variable save_env_var("TEST_VAR", "test_value") assert read_env_var("TEST_VAR") == "test_value" @@ -258,16 +261,13 @@ def test_env_var_read_write(tmp_path, monkeypatch): assert read_env_var("NON_EXISTENT_VAR") is None -def test_env_var_read_no_file(tmp_path, monkeypatch): +def test_env_var_read_no_file(mock_env_file): # noqa: ARG001 """Test reading environment variables when .env file doesn't exist.""" - # Mock USER_CONFIG_PATH to use an empty temporary directory - monkeypatch.setattr("sed.core.config.USER_CONFIG_PATH", tmp_path) - # Test reading from non-existent file assert read_env_var("TEST_VAR") is None -def test_env_var_special_characters(): +def test_env_var_special_characters(mock_env_file): # noqa: ARG001 """Test reading and writing environment variables with special characters.""" test_cases = { "TEST_URL": "http://example.com/path?query=value", @@ -280,50 +280,42 @@ def test_env_var_special_characters(): assert read_env_var(var_name) == value -@pytest.fixture -def cleanup_env_files(): - """Cleanup any .env files before and after tests""" - # Clean up any existing .env files - for path in [Path(".env"), USER_CONFIG_PATH / ".env"]: - if path.exists(): - path.unlink() - - yield - - # Clean up after tests - for path in [Path(".env"), USER_CONFIG_PATH / ".env"]: - if path.exists(): - path.unlink() - - -def test_env_var_precedence(cleanup_env_files): # noqa: ARG001 +def test_env_var_precedence(mock_env_file, tmp_path, monkeypatch): # noqa: ARG001 """Test that environment variables are read in correct order of precedence""" + # Create local .env directory if it doesn't exist + local_env_dir = tmp_path / "local" + local_env_dir.mkdir(exist_ok=True) + monkeypatch.setattr("sed.core.config.ENV_DIR", local_env_dir / ".env") + # Set up test values in different locations os.environ["TEST_VAR"] = "os_value" - with open(".env", "w") as f: - f.write("TEST_VAR=local_value\n") - - save_env_var("TEST_VAR", "user_value") # Saves to USER_CONFIG_PATH + # Save to user config first (lowest precedence) + save_env_var("TEST_VAR", "user_value") - # Should get OS value first - assert read_env_var("TEST_VAR") == "os_value" + # Create local .env file (medium precedence) + with open(local_env_dir / ".env", "w") as f: + f.write("TEST_VAR=local_value\n") - # Remove from OS env and should get local value - del os.environ["TEST_VAR"] + # Remove from OS env to test other precedence levels + monkeypatch.delenv("TEST_VAR", raising=False) assert read_env_var("TEST_VAR") == "local_value" # Remove local .env and should get user config value - Path(".env").unlink() + (local_env_dir / ".env").unlink() assert read_env_var("TEST_VAR") == "user_value" # Remove user config and should get None - (USER_CONFIG_PATH / ".env").unlink() + (mock_env_file / ".env").unlink() assert read_env_var("TEST_VAR") is None -def test_env_var_save_and_load(cleanup_env_files): # noqa: ARG001 +def test_env_var_save_and_load(mock_env_file, monkeypatch): # noqa: ARG001 """Test saving and loading environment variables""" + # Clear any existing OS environment variables + monkeypatch.delenv("TEST_VAR", raising=False) + monkeypatch.delenv("OTHER_VAR", raising=False) + # Save a variable save_env_var("TEST_VAR", "test_value") @@ -336,14 +328,20 @@ def test_env_var_save_and_load(cleanup_env_files): # noqa: ARG001 assert read_env_var("OTHER_VAR") == "other_value" -def test_env_var_not_found(cleanup_env_files): # noqa: ARG001 +def test_env_var_not_found(mock_env_file): # noqa: ARG001 """Test behavior when environment variable is not found""" assert read_env_var("NONEXISTENT_VAR") is None -def test_env_file_format(cleanup_env_files): # noqa: ARG001 +def test_env_file_format(mock_env_file, monkeypatch): # noqa: ARG001 """Test that .env file parsing handles different formats correctly""" - with open(".env", "w") as f: + # Clear any existing OS environment variables + monkeypatch.delenv("TEST_VAR", raising=False) + monkeypatch.delenv("SPACES_VAR", raising=False) + monkeypatch.delenv("EMPTY_VAR", raising=False) + monkeypatch.delenv("COMMENT", raising=False) + + with open(mock_env_file / ".env", "w") as f: f.write( """ TEST_VAR=value1 From 27234e06abc71be3342161c3f76c6081e1df8e6f Mon Sep 17 00:00:00 2001 From: rettigl Date: Sun, 19 Jan 2025 22:09:13 +0100 Subject: [PATCH 298/300] Fix release and update documentation - Update GitHub Actions workflow for documentation - Modify pyproject.toml for release adjustments --- .github/workflows/documentation.yml | 15 ++++++++++----- pyproject.toml | 10 +++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 11c905bc..da4be209 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -107,11 +107,16 @@ jobs: run: | if [[ $GITHUB_REF == refs/tags/* ]]; then VERSION=${GITHUB_REF#refs/tags/} - echo "folder=sed/$VERSION" >> $GITHUB_OUTPUT - rm docs-repo/sed/stable - rm -rf docs-repo/sed/latest - ln -s -r docs-repo/sed/$VERSION docs-repo/sed/stable - ln -s -r docs-repo/sed/$VERSION docs-repo/sed/latest + echo "folder=sed/$VERSION" >> $GITHUB_OUTPUT + if [[ $VERSION == *a* ]]; then + rm -rf docs-repo/sed/latest + ln -s -r docs-repo/sed/$VERSION docs-repo/sed/latest + else + rm -rf docs-repo/sed/stable + rm -rf docs-repo/sed/latest + ln -s -r docs-repo/sed/$VERSION docs-repo/sed/stable + ln -s -r docs-repo/sed/$VERSION docs-repo/sed/latest + fi elif [[ $GITHUB_REF == refs/heads/main ]]; then rm -rf docs-repo/sed/latest echo "folder=sed/latest" >> $GITHUB_OUTPUT diff --git a/pyproject.toml b/pyproject.toml index 6a852d9d..7aec864b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,16 @@ authors = [ ] readme = "README.md" keywords = ["sed", "mpes", "flash", "arpes"] -license = {text = "MIT"} +license = { file = "LICENSE" } requires-python = ">=3.9,<3.13" +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] dependencies = [ "bokeh>=2.4.2", "dask>=2021.12.0,<2024.8", From 321f2fe6d5d72bcb62d2350fbd1411351a73b9c4 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 20 Jan 2025 21:07:32 +0100 Subject: [PATCH 299/300] use user platformdir also for user config change user and system config files to config_v1.yaml add system wide .env --- docs/user_guide/config.md | 4 ++-- src/sed/core/config.py | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/user_guide/config.md b/docs/user_guide/config.md index bfc6e4da..27c53d67 100644 --- a/docs/user_guide/config.md +++ b/docs/user_guide/config.md @@ -4,8 +4,8 @@ The config module contains a mechanism to collect configuration parameters from It will load an (optional) provided config file, or alternatively use a passed python dictionary as initial config dictionary, and subsequently look for the following additional config files to load: * ``folder_config``: A config file of name :file:`sed_config.yaml` in the current working directory. This is mostly intended to pass calibration parameters of the workflow between different notebook instances. -* ``user_config``: A config file provided by the user, stored as :file:`.sed/config.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings. -* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/sed/config.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/sed/config.yaml` on Windows. This should provide all necessary default parameters for using the sed processor with a given setup. For an example for an mpes setup, see :ref:`example_config` +* ``user_config``: A config file provided by the user, stored as :file:`.config/sed/config_v1.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings. +* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/sed/config_v1.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/sed/config_v1.yaml` on Windows. This should provide all necessary default parameters for using the sed processor with a given setup. For an example for an mpes setup, see :ref:`example_config` * ``default_config``: The default configuration shipped with the package. Typically, all parameters here should be overwritten by any of the other configuration files. The config mechanism returns the combined dictionary, and reports the loaded configuration files. In order to disable or overwrite any of the configuration files, they can be also given as optional parameters (path to a file, or python dictionary). diff --git a/src/sed/core/config.py b/src/sed/core/config.py index 686ee5b3..42f21153 100644 --- a/src/sed/core/config.py +++ b/src/sed/core/config.py @@ -19,6 +19,11 @@ package_dir = os.path.dirname(find_spec("sed").origin) USER_CONFIG_PATH = user_config_path(appname="sed", appauthor="OpenCOMPES", ensure_exists=True) +SYSTEM_CONFIG_PATH = ( + Path(os.environ["ALLUSERSPROFILE"]).joinpath("sed") + if platform.system() == "Windows" + else Path("/etc/").joinpath("sed") +) # Configure logging logger = setup_logging("config") @@ -49,11 +54,11 @@ def parse_config( user_config (dict | str, optional): user-based config dictionary or file path. The loaded dictionary is completed with the user-based values, taking preference over system and default values. - Defaults to the file ".sed/config.yaml" in the current user's home directory. + Defaults to the file ".config/sed/config_v1.yaml" in the current user's home directory. system_config (dict | str, optional): system-wide config dictionary or file path. The loaded dictionary is completed with the system-wide values, - taking preference over default values. Defaults to the file "/etc/sed/config.yaml" - on linux, and "%ALLUSERSPROFILE%/sed/config.yaml" on windows. + taking preference over default values. Defaults to the file "/etc/sed/config_v1.yaml" + on linux, and "%ALLUSERSPROFILE%/sed/config_v1.yaml" on windows. default_config (dict | str, optional): default config dictionary or file path. The loaded dictionary is completed with the default values. Defaults to *package_dir*/config/default.yaml". @@ -93,9 +98,7 @@ def parse_config( user_dict = copy.deepcopy(user_config) else: if user_config is None: - user_config = str( - Path.home().joinpath(".sed").joinpath("config.yaml"), - ) + user_config = str(USER_CONFIG_PATH.joinpath("config_v1.yaml")) if Path(user_config).exists(): user_dict = load_config(user_config) if verbose: @@ -106,14 +109,7 @@ def parse_config( system_dict = copy.deepcopy(system_config) else: if system_config is None: - if platform.system() in ["Linux", "Darwin"]: - system_config = str( - Path("/etc/").joinpath("sed").joinpath("config.yaml"), - ) - elif platform.system() == "Windows": - system_config = str( - Path(os.environ["ALLUSERSPROFILE"]).joinpath("sed").joinpath("config.yaml"), - ) + system_config = str(SYSTEM_CONFIG_PATH.joinpath("config_v1.yaml")) if Path(system_config).exists(): system_dict = load_config(system_config) if verbose: @@ -281,6 +277,7 @@ def read_env_var(var_name: str) -> str | None: 1. OS environment variables 2. .env file in current directory 3. .env file in user config directory + 4. .env file in system config directory Args: var_name (str): Name of the environment variable to read @@ -288,24 +285,30 @@ def read_env_var(var_name: str) -> str | None: Returns: str | None: Value of the environment variable or None if not found """ - # First check OS environment variables + # 1. check OS environment variables value = os.getenv(var_name) if value is not None: logger.debug(f"Found {var_name} in OS environment variables") return value - # Then check .env in current directory + # 2. check .env in current directory local_vars = _parse_env_file(Path(".env")) if var_name in local_vars: logger.debug(f"Found {var_name} in ./.env file") return local_vars[var_name] - # Finally check .env in user config directory + # 3. check .env in user config directory user_vars = _parse_env_file(USER_CONFIG_PATH / ".env") if var_name in user_vars: logger.debug(f"Found {var_name} in user config .env file") return user_vars[var_name] + # 4. check .env in system config directory + system_vars = _parse_env_file(SYSTEM_CONFIG_PATH / ".env") + if var_name in system_vars: + logger.debug(f"Found {var_name} in system config .env file") + return system_vars[var_name] + logger.debug(f"Environment variable {var_name} not found in any location") return None From d283e935fed75bc9f7d06061ed660510b3134d59 Mon Sep 17 00:00:00 2001 From: rettigl Date: Mon, 20 Jan 2025 23:33:15 +0100 Subject: [PATCH 300/300] add tests for system config dir --- tests/test_config.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index fb04fb65..6753c2a1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -285,18 +285,27 @@ def test_env_var_precedence(mock_env_file, tmp_path, monkeypatch): # noqa: ARG0 # Create local .env directory if it doesn't exist local_env_dir = tmp_path / "local" local_env_dir.mkdir(exist_ok=True) + system_env_dir = tmp_path / "system" + system_env_dir.mkdir(exist_ok=True) monkeypatch.setattr("sed.core.config.ENV_DIR", local_env_dir / ".env") + monkeypatch.setattr("sed.core.config.SYSTEM_CONFIG_PATH", system_env_dir) # Set up test values in different locations os.environ["TEST_VAR"] = "os_value" - # Save to user config first (lowest precedence) + # Save to system config first (4th precedence) + with open(system_env_dir / ".env", "w") as f: + f.write("TEST_VAR=system_value\n") + + # Save to user config first (3rd precedence) save_env_var("TEST_VAR", "user_value") - # Create local .env file (medium precedence) + # Create local .env file (2nd precedence) with open(local_env_dir / ".env", "w") as f: f.write("TEST_VAR=local_value\n") + assert read_env_var("TEST_VAR") == "os_value" + # Remove from OS env to test other precedence levels monkeypatch.delenv("TEST_VAR", raising=False) assert read_env_var("TEST_VAR") == "local_value" @@ -305,8 +314,12 @@ def test_env_var_precedence(mock_env_file, tmp_path, monkeypatch): # noqa: ARG0 (local_env_dir / ".env").unlink() assert read_env_var("TEST_VAR") == "user_value" - # Remove user config and should get None + # Remove user config and should get system value (mock_env_file / ".env").unlink() + assert read_env_var("TEST_VAR") == "system_value" + + # Remove system config and should get None + (system_env_dir / ".env").unlink() assert read_env_var("TEST_VAR") is None