From 9e5e57743a7cfd33f9aeb1929293f130b64553d8 Mon Sep 17 00:00:00 2001 From: James S Date: Mon, 31 Mar 2025 14:04:23 +0100 Subject: [PATCH 01/14] quality of life updates --- .gitignore | 174 ++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 33 ------- doc/source/conf.py | 49 ++++------ poetry.lock => no-poetry.lock | 0 pyproject.toml | 40 ++------ ruff.toml | 41 ++++++++ src/fitter/fitter.py | 38 +++----- src/fitter/histfit.py | 6 +- src/fitter/main.py | 26 ++--- test/test_fitter.py | 15 +-- test/test_histfit.py | 34 +++---- test/test_main.py | 5 +- 12 files changed, 297 insertions(+), 164 deletions(-) create mode 100644 .gitignore delete mode 100644 .pre-commit-config.yaml rename poetry.lock => no-poetry.lock (100%) create mode 100644 ruff.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1800114 --- /dev/null +++ b/.gitignore @@ -0,0 +1,174 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 6707eac..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,33 +0,0 @@ - -files: '\.(py|rst|sh)$' -fail_fast: false - -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - #- id: check-executables-have-shebangs - - id: check-ast - -- repo: https://github.com/pycqa/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - args: ["-j8", "--ignore=E203,E501,W503,E722", "--max-line-length=120", "--exit-zero"] - -- repo: https://github.com/psf/black - rev: 22.10.0 - hooks: - - id: black - args: ["--line-length=120"] - exclude: E501 - -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: ["--profile", "black"] # solves conflicts between black and isort - diff --git a/doc/source/conf.py b/doc/source/conf.py index 8e9f77b..4e53b0c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # @@ -21,11 +20,12 @@ import pkg_resources + version = pkg_resources.require("fitter")[0].version -project = 'fitter' -copyright = '2019, Thomas Cokelaer' -author = 'Thomas Cokelaer' +project = "fitter" +copyright = "2019, Thomas Cokelaer" +author = "Thomas Cokelaer" # The short X.Y version version = version @@ -43,24 +43,24 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -72,7 +72,7 @@ # 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"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None @@ -83,7 +83,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -94,7 +94,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -110,7 +110,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'fitterdoc' +htmlhelp_basename = "fitterdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -119,15 +119,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -137,8 +134,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'fitter.tex', 'fitter Documentation', - 'Thomas Cokelaer', 'manual'), + (master_doc, "fitter.tex", "fitter Documentation", "Thomas Cokelaer", "manual"), ] @@ -147,8 +143,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'fitter', 'fitter Documentation', - [author], 1) + (master_doc, "fitter", "fitter Documentation", [author], 1), ] @@ -158,9 +153,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'fitter', 'fitter Documentation', - author, 'fitter', 'One line description of project.', - 'Miscellaneous'), + (master_doc, "fitter", "fitter Documentation", author, "fitter", "One line description of project.", "Miscellaneous"), ] @@ -179,7 +172,7 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- @@ -187,4 +180,4 @@ # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True \ No newline at end of file +todo_include_todos = True diff --git a/poetry.lock b/no-poetry.lock similarity index 100% rename from poetry.lock rename to no-poetry.lock diff --git a/pyproject.toml b/pyproject.toml index 8a1624b..897baf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fitter" -version = "1.7.1" +version = "1.7.1-numpy2" description = "A tool to fit data to many distributions and get the best one(s)" authors = ["Thomas Cokelaer "] license = "GPL" @@ -23,33 +23,11 @@ classifiers = [ "Topic :: Scientific/Engineering :: Information Analysis", ] - - -[tool.poetry.dependencies] -python = "^3.8" -click = "^8.1.6" -joblib = "^1.3.1" -matplotlib = "^3.7.2" -numpy = "^1.20.0" -pandas = ">= 0.23.4, <3.0.0" -scipy = ">=0.18.0, <2.0.0" -tqdm = "^4.65.1" -loguru = "^0.7.2" -rich-click = "^1.7.2" - -[tool.poetry.group.dev.dependencies] -pytest = "^7.4.0" -pytest-cov = "^4.1.0" -pytest-xdist = "^3.3.1" -pytest-mock = "^3.11.1" -pytest-timeout = "^1" -coveralls = "^3.3.1" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry.scripts] -fitter = "fitter.main:main" - - +[tool.isort] +profile = "black" +line_length = 88 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..6ee6041 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,41 @@ +# black formatter takes care of the line length +line-length = 999 + +lint.select = ["ALL"] + +lint.ignore = [ + "UP006", # https://github.com/charliermarsh/ruff/pull/4427 + "UP007", # https://github.com/charliermarsh/ruff/pull/4427 + # Mutable class attributes should be annotated with `typing.ClassVar` + # Too many violations + "RUF012", + # Logging statement uses f-string + "G004", + "T201", # flake8-print + "ERA001", # Commented out code + "W291", # trailing whitespace + "UP018" # native-literals (UP018). +] + +# Mininal python version we support is 3.13 +target-version = "py313" + +[lint.per-file-ignores] +# python scripts in bin/ needs some python path configurations before import +"bin/*.py" = [ + # E402: module-import-not-at-top-of-file + "E402", + # S603: `subprocess` call: check for execution of untrusted input + # these are dev tools and do not have risks of malicious inputs. + "S603", + # T201 `print` found + # print() is allowed in bin/ as they are dev tools. + "T201", +] + +[lint.pylint] +max-args = 6 # We have many functions reaching 6 args + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" \ No newline at end of file diff --git a/src/fitter/fitter.py b/src/fitter/fitter.py index 90e7085..0f6facc 100644 --- a/src/fitter/fitter.py +++ b/src/fitter/fitter.py @@ -18,10 +18,9 @@ .. sectionauthor:: Thomas Cokelaer, Aug 2014-2020 """ + import contextlib -import sys -import threading -from datetime import datetime +import multiprocessing import joblib import numpy as np @@ -33,9 +32,8 @@ from scipy.stats import entropy as kl_div from scipy.stats import kstest from tqdm import tqdm -import multiprocessing -__all__ = ["get_common_distributions", "get_distributions", "Fitter"] +__all__ = ["Fitter", "get_common_distributions", "get_distributions"] # A solution to wrap joblib parallel call in tqdm from @@ -44,8 +42,8 @@ @contextlib.contextmanager def tqdm_joblib(*args, **kwargs): """Context manager to patch joblib to report into tqdm progress bar - given as argument""" - + given as argument + """ tqdm_object = tqdm(*args, **kwargs) class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): @@ -92,7 +90,7 @@ def get_common_distributions(): return common -class Fitter(object): +class Fitter: """Fit a data sample to known distributions A naive approach often performed to figure out the undelying distribution that @@ -250,9 +248,7 @@ def _get_xmin(self): return self._xmin def _set_xmin(self, value): - if value == None: - value = self._alldata.min() - elif value < self._alldata.min(): + if value == None or value < self._alldata.min(): value = self._alldata.min() self._xmin = value self._trim_data() @@ -264,9 +260,7 @@ def _get_xmax(self): return self._xmax def _set_xmax(self, value): - if value == None: - value = self._alldata.max() - elif value > self._alldata.max(): + if value == None or value > self._alldata.max(): value = self._alldata.max() self._xmax = value self._trim_data() @@ -281,7 +275,6 @@ def _load_all_distributions(self): def hist(self): """Draw normed histogram of the data using :attr:`bins` - .. plot:: >>> from scipy import stats @@ -330,11 +323,11 @@ def _fit_single_distribution(distribution, data, x, y, timeout): dist_fitted = dist(*param) ks_stat, ks_pval = kstest(data, dist_fitted.cdf) - logger.info("Fitted {} distribution with error={})".format(distribution, round(sq_error, 6))) + logger.info(f"Fitted {distribution} distribution with error={round(sq_error, 6)})") return distribution, (param, pdf_fitted, sq_error, aic, bic, kullback_leibler, ks_stat, ks_pval) except Exception: # pragma: no cover - logger.warning("SKIPPED {} distribution (taking more than {} seconds)".format(distribution, timeout)) + logger.warning(f"SKIPPED {distribution} distribution (taking more than {timeout} seconds)") return distribution, None @@ -354,9 +347,7 @@ def fit(self, progress=False, n_jobs=-1, max_workers=-1, prefer="processes"): """ N = len(self.distributions) with tqdm_joblib(desc=f"Fitting {N} distributions", total=N, disable=not progress) as progress_bar: - results = Parallel(n_jobs=max_workers, prefer=prefer)( - delayed(Fitter._fit_single_distribution)(dist, self._data, self.x, self.y, self.timeout) for dist in self.distributions - ) + results = Parallel(n_jobs=max_workers, prefer=prefer)(delayed(Fitter._fit_single_distribution)(dist, self._data, self.x, self.y, self.timeout) for dist in self.distributions) for distribution, values in results: if values is not None: @@ -384,7 +375,7 @@ def fit(self, progress=False, n_jobs=-1, max_workers=-1, prefer="processes"): "kl_div": self._kldiv, "ks_statistic": self._ks_stat, "ks_pvalue": self._ks_pval, - } + }, ) self.df_errors.sort_index(inplace=True) @@ -398,8 +389,7 @@ def plot_pdf(self, names=None, Nbest=5, lw=2, method="sumsquare_error"): """ assert Nbest > 0 - if Nbest > len(self.distributions): - Nbest = len(self.distributions) + Nbest = min(Nbest, len(self.distributions)) if isinstance(names, list): for name in names: @@ -433,7 +423,7 @@ def get_best(self, method="sumsquare_error"): param_names = (distribution.shapes + ", loc, scale").split(", ") if distribution.shapes else ["loc", "scale"] param_dict = {} - for d_key, d_val in zip(param_names, params): + for d_key, d_val in zip(param_names, params, strict=False): param_dict[d_key] = d_val return {name: param_dict} diff --git a/src/fitter/histfit.py b/src/fitter/histfit.py index 9b3c464..f102daa 100644 --- a/src/fitter/histfit.py +++ b/src/fitter/histfit.py @@ -1,7 +1,6 @@ -import scipy.stats import pylab -from pylab import mean, sqrt, std - +import scipy.stats +from pylab import mean, sqrt __all__ = ["HistFit"] @@ -62,7 +61,6 @@ def __init__(self, data=None, X=None, Y=None, bins=None): hist function and bins may be provided. """ - self.data = data if data: Y, X, _ = pylab.hist(self.data, bins=bins, density=True) diff --git a/src/fitter/main.py b/src/fitter/main.py index 12b029f..c038a1a 100644 --- a/src/fitter/main.py +++ b/src/fitter/main.py @@ -15,13 +15,9 @@ # ############################################################################## """.. rubric:: Standalone application""" + import csv -import glob -import json -import os -import subprocess import sys -import textwrap from pathlib import Path import rich_click as click @@ -44,23 +40,29 @@ @click.group(context_settings=CONTEXT_SETTINGS) @click.version_option(version=version) def main(): # pragma: no cover - """fitter can fit your data using Scipy distributions + """Fitter can fit your data using Scipy distributions Example: - fitter fitdist data.csv """ - pass @main.command() @click.argument("filename", type=click.STRING) @click.option( - "--column-number", type=click.INT, default=1, show_default=True, help="data column to use (first column by default)" + "--column-number", + type=click.INT, + default=1, + show_default=True, + help="data column to use (first column by default)", ) @click.option( - "--delimiter", type=click.STRING, default=",", show_default=True, help="column delimiter (comma by default)" + "--delimiter", + type=click.STRING, + default=",", + show_default=True, + help="column delimiter (comma by default)", ) @click.option( "--distributions", @@ -74,11 +76,11 @@ def main(): # pragma: no cover @click.option("--verbose/--no-verbose", default=True, show_default=True) @click.option("--output-image", type=click.STRING, default="fitter.png", show_default=True) def fitdist(**kwargs): - """fit distribution""" + """Fit distribution""" from pylab import savefig col = kwargs["column_number"] - with open(kwargs["filename"], "r") as csvfile: + with open(kwargs["filename"]) as csvfile: data = csv.reader(csvfile, delimiter=kwargs["delimiter"]) data = [float(x[col - 1]) for x in data] diff --git a/test/test_fitter.py b/test/test_fitter.py index 9fac889..a3a2472 100644 --- a/test/test_fitter.py +++ b/test/test_fitter.py @@ -2,12 +2,12 @@ def test_dist(): - assert 'gamma' in get_common_distributions() + assert "gamma" in get_common_distributions() assert len(get_distributions()) > 40 def test_fitter(): - f = Fitter([1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3], distributions=['gamma'], xmin=0, xmax=4) + f = Fitter([1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3], distributions=["gamma"], xmin=0, xmax=4) try: f.plot_pdf() except Exception: @@ -23,7 +23,7 @@ def test_fitter(): assert f.xmin == 1 assert f.xmax == 3 - f = Fitter([1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3], distributions=['gamma']) + f = Fitter([1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3], distributions=["gamma"]) f.fit(progress=True) f.summary() assert f.xmin == 1 @@ -32,6 +32,7 @@ def test_fitter(): def test_gamma(): from scipy import stats + data = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) f = Fitter(data, bins=100) @@ -39,7 +40,7 @@ def test_gamma(): f.xmax = 1000000 # no effect f.xmin = 0.1 f.xmax = 10 - f.distributions = ['gamma', "alpha"] + f.distributions = ["gamma", "alpha"] f.fit() df = f.summary() assert len(df) @@ -53,18 +54,20 @@ def test_gamma(): def test_others(): from scipy import stats + data = stats.gamma.rvs(2, loc=1.5, scale=2, size=1000) f = Fitter(data, bins=100, distributions="common") f.fit() - assert f.df_errors.loc["gamma"].loc['aic'] > 100 + assert f.df_errors.loc["gamma"].loc["aic"] > 100 f = Fitter(data, bins=100, distributions="gamma") f.fit() - assert f.df_errors.loc["gamma"].loc['aic'] > 100 + assert f.df_errors.loc["gamma"].loc["aic"] > 100 def test_n_jobs_api(): from scipy import stats + data = stats.gamma.rvs(2, loc=1.5, scale=2, size=1000) f = Fitter(data, distributions="common") f.fit(n_jobs=-1) diff --git a/test/test_histfit.py b/test/test_histfit.py index 29fef49..405b313 100644 --- a/test/test_histfit.py +++ b/test/test_histfit.py @@ -1,34 +1,22 @@ -from fitter import Fitter, get_distributions, get_common_distributions - - - def test1(): - from fitter import HistFit import scipy.stats - data = [scipy.stats.norm.rvs(2,3.4) for x in range(10000)] + + from fitter import HistFit + + data = [scipy.stats.norm.rvs(2, 3.4) for x in range(10000)] hf = HistFit(data, bins=30) - hf.fit(error_rate=0.03, Nfit=20 ) + hf.fit(error_rate=0.03, Nfit=20) print(hf.mu, hf.sigma, hf.amplitude) - + def test2(): - from fitter import HistFit - from pylab import hist import scipy.stats - data = [scipy.stats.norm.rvs(2,3.4) for x in range(10000)] + from pylab import hist + + from fitter import HistFit + + data = [scipy.stats.norm.rvs(2, 3.4) for x in range(10000)] Y, X, _ = hist(data, bins=30) hf = HistFit(X=X, Y=Y) hf.fit(error_rate=0.03, Nfit=20, semilogy=True) print(hf.mu, hf.sigma, hf.amplitude) - - - - - - - - - - - - diff --git a/test/test_main.py b/test/test_main.py index e4f0dea..7ff7347 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1,4 +1,3 @@ -import subprocess from pathlib import Path import pytest @@ -13,8 +12,8 @@ def setup_teardown(): data1 = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) data2 = stats.gamma.rvs(1, loc=1.5, scale=3, size=10000) with open("test.csv", "w") as tmp: - for x, y in zip(data1, data2): - tmp.write("{},{}\n".format(x, y)) + for x, y in zip(data1, data2, strict=False): + tmp.write(f"{x},{y}\n") # hand over control to test yield From 636dfbcf402bdd803b66342969adbeffb4c265f3 Mon Sep 17 00:00:00 2001 From: James S Date: Mon, 31 Mar 2025 14:17:41 +0100 Subject: [PATCH 02/14] qol update 2 --- pyproject.toml | 77 ++++++++++++++++++++++++++++++++++++++---------- requirements.txt | 10 +++++++ 2 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 897baf2..b0f6eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,50 @@ [tool.poetry] name = "fitter" -version = "1.7.1-numpy2" +version = "1.8.0" # Bumped version for numpy 2 compatibility description = "A tool to fit data to many distributions and get the best one(s)" authors = ["Thomas Cokelaer "] license = "GPL" readme = "README.rst" -keywords = ["fit", "distribution", "fitting", "scipy"] +repository = "https://github.com/cokelaer/fitter" +documentation = "https://fitter.readthedocs.io" +keywords = ["fit", "distribution", "fitting", "scipy", "statistics"] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Education", - "Intended Audience :: End Users/Desktop", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Scientific/Engineering :: Bio-Informatics", - "Topic :: Scientific/Engineering :: Information Analysis", + "Development Status :: 4 - Beta", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Information Analysis", ] +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +numpy = ">=2.0.0" +scipy = ">=1.10.0" +matplotlib = ">=3.5.0" +pandas = ">=1.3.0" +pytest = {version = ">=7.0.0", optional = true} + +[tool.poetry.group.dev.dependencies] +pytest = ">=7.0.0" +pytest-cov = ">=4.0.0" +black = ">=23.0.0" +isort = ">=5.12.0" +mypy = ">=1.0.0" +pre-commit = ">=3.0.0" + +[tool.poetry.extras] +test = ["pytest", "pytest-cov"] + [tool.isort] profile = "black" line_length = 88 @@ -31,3 +53,26 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true + +[tool.black] +line-length = 88 +target-version = ["py38", "py39", "py310", "py311", "py312"] + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +fitter = "fitter.main:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +addopts = "--cov=fitter --cov-report=term-missing" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e5d1790 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +click>=8.1.8 +joblib>=1.4.2 +loguru>=0.7.3 +numpy>=2.2.4 +pandas>=2.2.3 +pytest>=8.3.5 +rich_click>=1.8.8 +scipy>=1.15.2 +Sphinx>=8.2.3 +tqdm>=4.67.1 From 7c109ba94b6c395ff36df2b2b7408855317c7287 Mon Sep 17 00:00:00 2001 From: James S Date: Mon, 31 Mar 2025 17:52:30 +0100 Subject: [PATCH 03/14] use numpy --- requirements.txt | 1 + src/fitter/fitter.py | 1 + src/fitter/histfit.py | 9 +++++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index e5d1790..5c1b1de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ click>=8.1.8 joblib>=1.4.2 loguru>=0.7.3 +numba>=0.61.0rc2 numpy>=2.2.4 pandas>=2.2.3 pytest>=8.3.5 diff --git a/src/fitter/fitter.py b/src/fitter/fitter.py index 0f6facc..c268979 100644 --- a/src/fitter/fitter.py +++ b/src/fitter/fitter.py @@ -32,6 +32,7 @@ from scipy.stats import entropy as kl_div from scipy.stats import kstest from tqdm import tqdm +from numba import jit, prange __all__ = ["Fitter", "get_common_distributions", "get_distributions"] diff --git a/src/fitter/histfit.py b/src/fitter/histfit.py index f102daa..99bdc0e 100644 --- a/src/fitter/histfit.py +++ b/src/fitter/histfit.py @@ -1,3 +1,4 @@ +import numpy as np import pylab import scipy.stats from pylab import mean, sqrt @@ -68,8 +69,8 @@ def __init__(self, data=None, X=None, Y=None, bins=None): self.X = [(X[i] + X[i + 1]) / 2 for i in range(self.N)] self.Y = Y self.A = 1 - self.guess_std = pylab.std(self.data) - self.guess_mean = pylab.mean(self.data) + self.guess_std = np.std(self.data) + self.guess_mean = np.mean(self.data) self.guess_amp = 1 else: self.X = X @@ -80,7 +81,7 @@ def __init__(self, data=None, X=None, Y=None, bins=None): self.N = len(self.X) self.guess_mean = self.X[int(self.N / 2)] - self.guess_std = sqrt(sum((self.X - mean(self.X)) ** 2) / self.N) / (sqrt(2 * 3.14)) + self.guess_std = sqrt(sum((self.X - np.mean(self.X)) ** 2) / self.N) / (sqrt(2 * 3.14)) self.guess_amp = 1.0 self.func = self._func_normal @@ -127,7 +128,7 @@ def fit( pylab.figure(2) pylab.clf() # pylab.bar(self.X, self.Y, width=0.85, ec="k", alpha=0.5) - M = mean(self.fits, axis=0) + M = np.mean(self.fits, axis=0) S = pylab.std(self.fits, axis=0) pylab.fill_between(self.X, M - 3 * S, M + 3 * S, color="gray", alpha=0.5) pylab.fill_between(self.X, M - 2 * S, M + 2 * S, color="gray", alpha=0.5) From 7cd69c2c6a239b69646e0aabc102772934c9d56a Mon Sep 17 00:00:00 2001 From: James S Date: Wed, 2 Apr 2025 15:02:13 +0100 Subject: [PATCH 04/14] fix and update unit tests --- pyproject.toml | 5 -- src/fitter/fitter.py | 19 +++--- test/run_tests.py | 148 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 test/run_tests.py diff --git a/pyproject.toml b/pyproject.toml index b0f6eef..ae7e6c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,3 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] fitter = "fitter.main:main" - -[tool.pytest.ini_options] -testpaths = ["tests"] -python_files = "test_*.py" -addopts = "--cov=fitter --cov-report=term-missing" \ No newline at end of file diff --git a/src/fitter/fitter.py b/src/fitter/fitter.py index c268979..3402b42 100644 --- a/src/fitter/fitter.py +++ b/src/fitter/fitter.py @@ -200,7 +200,7 @@ def __init__( #: list of distributions to test self.distributions = distributions - if self.distributions == None: + if self.distributions is None: self._load_all_distributions() elif self.distributions == "common": self.distributions = get_common_distributions() @@ -210,11 +210,11 @@ def __init__( self.bins = bins self._alldata = np.array(data) - if xmin == None: + if xmin is None: self._xmin = self._alldata.min() else: self._xmin = xmin - if xmax == None: + if xmax is None: self._xmax = self._alldata.max() else: self._xmax = xmax @@ -249,7 +249,7 @@ def _get_xmin(self): return self._xmin def _set_xmin(self, value): - if value == None or value < self._alldata.min(): + if value is None or value < self._alldata.min(): value = self._alldata.min() self._xmin = value self._trim_data() @@ -261,7 +261,7 @@ def _get_xmax(self): return self._xmax def _set_xmax(self, value): - if value == None or value > self._alldata.max(): + if value is None or value > self._alldata.max(): value = self._alldata.max() self._xmax = value self._trim_data() @@ -304,7 +304,7 @@ def _fit_single_distribution(distribution, data, x, y, timeout): pdf_fitted = dist.pdf(x, *param) # calculate error - sq_error = pylab.sum((pdf_fitted - y) ** 2) + sq_error = np.sum((pdf_fitted - y) ** 2) # calculate information criteria logLik = np.sum(dist.logpdf(x, *param)) @@ -315,7 +315,7 @@ def _fit_single_distribution(distribution, data, x, y, timeout): # special case of gaussian distribution # bic = n * np.log(sq_error / n) + k * np.log(n) # general case: - bic = k * pylab.log(n) - 2 * logLik + bic = k * np.log(n) - 2 * logLik # calculate kullback leibler divergence kullback_leibler = kl_div(pdf_fitted, y) @@ -407,7 +407,7 @@ def plot_pdf(self, names=None, Nbest=5, lw=2, method="sumsquare_error"): if name in self.fitted_pdf.keys(): pylab.plot(self.x, self.fitted_pdf[name], lw=lw, label=name) else: # pragma: no cover - logger.warning("%s was not fitted. no parameters available" % name) + logger.warning(f"{name} was not fitted. no parameters available") pylab.grid(True) pylab.legend() @@ -446,7 +446,8 @@ def summary(self, Nbest=5, lw=2, plot=True, method="sumsquare_error", clf=True): @staticmethod def _with_timeout(func, args=(), kwargs={}, timeout=30): - with multiprocessing.pool.ThreadPool(1) as pool: + n_workers = multiprocessing.cpu_count() # Get number of available cores instead of hardcoding to 1 + with multiprocessing.pool.ThreadPool(n_workers) as pool: async_result = pool.apply_async(func, args, kwargs) return async_result.get(timeout=timeout) diff --git a/test/run_tests.py b/test/run_tests.py new file mode 100644 index 0000000..a5c92b3 --- /dev/null +++ b/test/run_tests.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +""" +Test Runner for Python Projects +Discovers and runs all tests in the current directory +Supports both unittest and pytest test files +""" + +import os +import sys +import argparse +import glob +import subprocess +import time +from pathlib import Path + + +def run_pytest(test_dir=".", pattern="test_*.py", verbose=False, specific_file=None): + """ + Run pytest on files matching the pattern in the specified directory + + Parameters: + ----------- + test_dir : str + Directory containing test files (default: current directory) + pattern : str + Pattern to match test files (default: test_*.py) + verbose : bool + Whether to use verbose output + specific_file : str + Run a specific test file + + Returns: + -------- + bool + True if all tests pass, False otherwise + """ + start_time = time.time() + + # Check if pytest is installed + try: + import pytest + except ImportError: + print("pytest is not installed. Install it with: pip install pytest") + return False + + # Find project root (looking for src directory or setup.py) + project_root = os.getcwd() + while project_root and not ( + os.path.exists(os.path.join(project_root, "src")) or + os.path.exists(os.path.join(project_root, "setup.py")) + ): + parent = os.path.dirname(project_root) + if parent == project_root: # Reached filesystem root + break + project_root = parent + + print(f"Using project root: {project_root}") + + # Add src to Python path if it exists + src_path = os.path.join(project_root, "src") + if os.path.exists(src_path) and src_path not in sys.path: + sys.path.insert(0, src_path) + print(f"Added src directory to Python path: {src_path}") + + # Prepare pytest arguments + pytest_args = ["-v"] if verbose else [] + + if specific_file: + print(f"Running tests from file: {specific_file}") + if not os.path.exists(specific_file): + print(f"Error: Test file '{specific_file}' does not exist.") + return False + test_files = [specific_file] + else: + print(f"Discovering tests in directory '{test_dir}' with pattern '{pattern}'...") + # Find all test files matching the pattern + test_files = glob.glob(os.path.join(test_dir, pattern)) + if not test_files: + print(f"No test files matching '{pattern}' found in '{test_dir}'.") + return False + + # Add files to pytest arguments + pytest_args.extend(test_files) + + print(f"Found {len(test_files)} test files") + for file in test_files: + print(f" - {file}") + + # Run pytest + print("\nRunning tests with pytest...") + # Set up environment variables for the subprocess + env = os.environ.copy() + + # Add src directory to PYTHONPATH if it exists + src_path = os.path.join(project_root, "src") + if os.path.exists(src_path): + pythonpath = env.get("PYTHONPATH", "") + if pythonpath: + env["PYTHONPATH"] = f"{src_path}{os.pathsep}{pythonpath}" + else: + env["PYTHONPATH"] = src_path + print(f"Added src directory to PYTHONPATH: {src_path}") + + # Use subprocess instead of pytest.main to avoid argument conflicts with pyproject.toml config + cmd = [sys.executable, "-m", "pytest"] + pytest_args + print(f"Running command: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=not verbose, env=env) + + if verbose: + # Output already shown + pass + elif result.returncode != 0: + print("Tests failed. Showing output:") + print(result.stdout.decode('utf-8')) + print(result.stderr.decode('utf-8')) + + # Report results + elapsed_time = time.time() - start_time + print(f"\nTest run completed in {elapsed_time:.2f} seconds") + + return result.returncode == 0 + + +def main(): + """Main entry point for the test runner""" + parser = argparse.ArgumentParser(description="Run tests for Python projects") + parser.add_argument("-d", "--directory", default=".", + help="Directory containing test files (default: current directory)") + parser.add_argument("-p", "--pattern", default="test_*.py", + help="Pattern to match test files (default: test_*.py)") + parser.add_argument("-v", "--verbose", action="store_true", + help="Increase verbosity") + parser.add_argument("-f", "--file", help="Run a specific test file") + args = parser.parse_args() + + success = run_pytest( + test_dir=args.directory, + pattern=args.pattern, + verbose=args.verbose, + specific_file=args.file + ) + + # Return appropriate exit code + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() \ No newline at end of file From 39d5dd987fb5febf44c96a5dd6c098d143a67cfe Mon Sep 17 00:00:00 2001 From: James S Date: Wed, 2 Apr 2025 15:23:50 +0100 Subject: [PATCH 05/14] slight updates --- README.rst | 28 +++++++++- src/fitter/fitter.py | 124 +++++++++++++++++++++++++++++++++---------- test/run_tests.py | 4 +- 3 files changed, 125 insertions(+), 31 deletions(-) diff --git a/README.rst b/README.rst index 39e1fdf..c7213b8 100644 --- a/README.rst +++ b/README.rst @@ -28,12 +28,36 @@ What is it ? The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. +## Running Tests + +To run the test suite for fitter, follow these steps: + +1. Clone the repository and install dev dependencies: + ```bash + git clone https://github.com/tg12/fitter + cd fitter + pip3 install -e ".[dev]" + ``` + +2. Navigate to the tests directory: + ```bash + cd tests + ``` + +3. Run the test suite using the provided test runner: + ```bash + python3 run_tests.py + ``` + +The test runner automatically handles src directory structures and ensures proper import paths are configured. + + Installation ################### :: - - pip install fitter + git clone https://github.com/tg12/fitter + pip install . **fitter** is also available on **conda** (bioconda channel):: diff --git a/src/fitter/fitter.py b/src/fitter/fitter.py index 3402b42..69f5bad 100644 --- a/src/fitter/fitter.py +++ b/src/fitter/fitter.py @@ -29,10 +29,10 @@ import scipy.stats from joblib.parallel import Parallel, delayed from loguru import logger +from numba import jit, prange from scipy.stats import entropy as kl_div from scipy.stats import kstest from tqdm import tqdm -from numba import jit, prange __all__ = ["Fitter", "get_common_distributions", "get_distributions"] @@ -45,37 +45,96 @@ def tqdm_joblib(*args, **kwargs): """Context manager to patch joblib to report into tqdm progress bar given as argument """ - tqdm_object = tqdm(*args, **kwargs) - - class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __call__(self, *args, **kwargs): - tqdm_object.update(n=self.batch_size) - return super().__call__(*args, **kwargs) - - old_batch_callback = joblib.parallel.BatchCompletionCallBack - joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback + # Only create progress bar if not disabled (saves overhead when progress tracking is off) + disable = kwargs.get('disable', False) + tqdm_object = tqdm(*args, **kwargs) if not disable else None + + if not disable: + class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): + def __call__(self, *args, **kwargs): + tqdm_object.update(n=self.batch_size) + return super().__call__(*args, **kwargs) + + old_batch_callback = joblib.parallel.BatchCompletionCallBack + joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback + try: yield tqdm_object finally: - joblib.parallel.BatchCompletionCallBack = old_batch_callback - tqdm_object.close() + if not disable: + joblib.parallel.BatchCompletionCallBack = old_batch_callback + tqdm_object.close() -def get_distributions(): +def get_distributions(verbose=False): + """ + Discover all available distributions in scipy.stats that have a 'fit' method. + + Parameters: + ----------- + verbose : bool + Whether to print discovery information during execution + + Returns: + -------- + list + List of distribution names that have a 'fit' method + """ + + distributions = [] - for this in dir(scipy.stats): - if "fit" in eval("dir(scipy.stats." + this + ")"): - distributions.append(this) + skipped = [] + + if verbose: + logger.info("Searching for distributions in scipy.stats with a 'fit' method...") + + for dist_name in dir(scipy.stats): + try: + # Skip private attributes, functions, and non-distribution objects + if dist_name.startswith('_') or dist_name in ['test', 'freeze']: + continue + + # Get the attribute + dist_obj = getattr(scipy.stats, dist_name) + + # Check if it's a distribution-like object with a fit method + if hasattr(dist_obj, 'fit'): + distributions.append(dist_name) + if verbose: + logger.debug(f"Found distribution: {dist_name}") + else: + skipped.append(dist_name) + + except Exception as e: + # Safely handle any unexpected errors during discovery + skipped.append(dist_name) + if verbose: + logger.warning(f"Error checking {dist_name}: {str(e)}") + + if verbose: + logger.success(f"Found {len(distributions)} distributions with a 'fit' method") + return distributions -def get_common_distributions(): - distributions = get_distributions() - # to avoid error due to changes in scipy - common = [ +def get_common_distributions(verbose=False): + """ + Get a curated list of common distributions that have a 'fit' method. + + Parameters: + ----------- + verbose : bool + Whether to print information during execution + + Returns: + -------- + list + List of common distribution names that have a 'fit' method + """ + from loguru import logger + + # Define the list of common distributions + common_distributions = [ "cauchy", "chi2", "expon", @@ -87,8 +146,20 @@ def get_common_distributions(): "rayleigh", "uniform", ] - common = [x for x in common if x in distributions] - return common + + # Get all available distributions + all_distributions = get_distributions(verbose=False) + + # Filter to only include distributions that exist in scipy.stats + available_common = [dist for dist in common_distributions if dist in all_distributions] + + if verbose: + logger.info(f"Found {len(available_common)} common distributions out of {len(all_distributions)} total") + if len(available_common) < len(common_distributions): + missing = set(common_distributions) - set(available_common) + logger.warning(f"Missing common distributions: {', '.join(missing)}") + + return available_common class Fitter: @@ -222,8 +293,7 @@ def __init__( self._trim_data() self._update_data_pdf() - # Other attributes - self._init() + self._init() # Other attributes def _init(self): self.fitted_param = {} diff --git a/test/run_tests.py b/test/run_tests.py index a5c92b3..4050d9d 100644 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -5,11 +5,11 @@ Supports both unittest and pytest test files """ -import os -import sys import argparse import glob +import os import subprocess +import sys import time from pathlib import Path From 2cd97ddbab4ac30faac004bd493ad38fc5919ea5 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:25:36 +0100 Subject: [PATCH 06/14] Update README.rst --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index c7213b8..4f23ff8 100644 --- a/README.rst +++ b/README.rst @@ -23,8 +23,8 @@ FITTER documentation Compatible with Python 3.7, and 3.8, 3.9 -What is it ? -################ +# What is it ? + The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. @@ -33,7 +33,8 @@ The **fitter** package is a Python library used for fitting probability distribu To run the test suite for fitter, follow these steps: 1. Clone the repository and install dev dependencies: - ```bash + + ```bash git clone https://github.com/tg12/fitter cd fitter pip3 install -e ".[dev]" From 949222c931cbe9811fc30b11be3b747a4f47e365 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:26:44 +0100 Subject: [PATCH 07/14] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4f23ff8..e48873e 100644 --- a/README.rst +++ b/README.rst @@ -18,9 +18,9 @@ FITTER documentation :alt: Documentation Status .. image:: https://zenodo.org/badge/23078551.svg - :target: https://zenodo.org/badge/latestdoi/23078551 + :target: https://zenodo.org/badge/latestdoi/23078551 -Compatible with Python 3.7, and 3.8, 3.9 +**Compatible with Python 3.7, 3.8, 3.9, and 3.10** # What is it ? From 9be4f453e335a25ebb83843cdd336c1e30aba5e6 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:27:01 +0100 Subject: [PATCH 08/14] Update README.rst --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e48873e..2bc808c 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ - - ############################# FITTER documentation ############################# @@ -23,12 +21,12 @@ FITTER documentation **Compatible with Python 3.7, 3.8, 3.9, and 3.10** -# What is it ? +#What is it ? The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. -## Running Tests +##Running Tests To run the test suite for fitter, follow these steps: From 6dc22a9d4f2f68dd4a76e32fbd454ce64ee08e6c Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:28:55 +0100 Subject: [PATCH 09/14] Update README.rst --- README.rst | 317 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 193 insertions(+), 124 deletions(-) diff --git a/README.rst b/README.rst index 2bc808c..c3680b1 100644 --- a/README.rst +++ b/README.rst @@ -1,175 +1,244 @@ -############################# -FITTER documentation +############################# +FITTER Documentation ############################# -.. image:: https://badge.fury.io/py/fitter.svg - :target: https://pypi.python.org/pypi/fitter +.. image:: https://badge.fury.io/py/fitter.svg :target: https://pypi.python.org/pypi/fitter -.. image:: https://github.com/cokelaer/fitter/actions/workflows/main.yml/badge.svg?branch=main - :target: https://github.com/cokelaer/fitter/actions/workflows/main.yml +.. image:: https://github.com/cokelaer/fitter/actions/workflows/main.yml/badge.svg?branch=main :target: https://github.com/cokelaer/fitter/actions/workflows/main.yml -.. image:: https://coveralls.io/repos/cokelaer/fitter/badge.png?branch=main - :target: https://coveralls.io/r/cokelaer/fitter?branch=main +.. image:: https://coveralls.io/repos/cokelaer/fitter/badge.png?branch=main :target: https://coveralls.io/r/cokelaer/fitter?branch=main -.. image:: http://readthedocs.org/projects/fitter/badge/?version=latest - :target: http://fitter.readthedocs.org/en/latest/?badge=latest - :alt: Documentation Status +.. image:: http://readthedocs.org/projects/fitter/badge/?version=latest :target: http://fitter.readthedocs.org/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://zenodo.org/badge/23078551.svg - :target: https://zenodo.org/badge/latestdoi/23078551 +.. image:: https://zenodo.org/badge/23078551.svg :target: https://zenodo.org/badge/latestdoi/23078551 **Compatible with Python 3.7, 3.8, 3.9, and 3.10** +# What is it? + +The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. + +Using **fitter**, you can easily: + +- Fit a range of distributions to your data +- Compare their fit quality using multiple metrics (SSE, AIC, BIC, KL divergence) +- Identify the most suitable distribution for your dataset + +The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. + +# Installation + +You can install fitter in several ways: + +From GitHub:: + +``` +git clone https://github.com/tg12/fitter +cd fitter +pip install . + +``` + +From PyPI:: + +``` +pip install fitter + +``` + +From Conda (bioconda channel):: + +``` +conda install fitter + +``` + +# Usage + +## Standalone Application + +A standalone application is provided that works with input CSV files:: + +``` +fitter fitdist data.csv --column-number 1 --distributions gamma,normal + +``` + +This creates: + +- A file called `fitter.png` with the visualization +- A log file `fitter.log` + +## From Python Shell + +First, let's create a sample dataset with 10,000 points from a gamma distribution: + +```python +from scipy import stats +data = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) + +``` + +_Note: The fitting process can be computationally intensive, so keep the sample size reasonable._ -#What is it ? +Now, without any prior knowledge about the distribution or its parameters, we can use **Fitter** to find the best fit: +```python +from fitter import Fitter +f = Fitter(data) +f.fit() +# This may take some time since by default, all distributions are tried +# You can manually provide a smaller set of distributions to speed up the process +f.summary() -The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. +``` -##Running Tests +![Fitter Example](http://pythonhosted.org/fitter/_images/index-1.png) + +See the [online documentation](http://fitter.readthedocs.io/) for more details. + +# Running Tests To run the test suite for fitter, follow these steps: -1. Clone the repository and install dev dependencies: - - ```bash - git clone https://github.com/tg12/fitter - cd fitter - pip3 install -e ".[dev]" - ``` +1. Clone the repository and install development dependencies: + +```bash +git clone https://github.com/tg12/fitter +cd fitter +pip3 install -e ".[dev]" + +``` + +2. Navigate to the tests directory: + +```bash +cd tests -2. Navigate to the tests directory: - ```bash - cd tests - ``` +``` -3. Run the test suite using the provided test runner: - ```bash - python3 run_tests.py - ``` +3. Run the test suite using the provided test runner: + +```bash +python3 run_tests.py + +``` + +### Additional Test Options + +The test runner supports several useful options: + +- Run with verbose output: + + ```bash + python3 run_tests.py -v + + ``` + +- Run a specific test file: + + ```bash + python3 run_tests.py -f test_fitter.py + + ``` + +- Run tests matching a specific keyword: + + ```bash + python3 run_tests.py -k "common" + + ``` + +- Specify an alternative test directory: + + ```bash + python3 run_tests.py -d ../other_tests + + ``` + The test runner automatically handles src directory structures and ensures proper import paths are configured. +# Contributors + +Setting up and maintaining Fitter has been possible thanks to users and contributors. Thanks to all: + +.. image:: https://contrib.rocks/image?repo=cokelaer/fitter :target: https://github.com/cokelaer/fitter/graphs/contributors + +# Changelog + +Version + +Description + +1.7.1 + +* Integrate PR github.com/cokelaer/fitter/pull/100 from @vitorandreazza to speedup multiprocessing run. + +1.7.0 + +* Replace logging with loguru
* Main application update to add missing --output-image option and use rich_click
* Replace pkg_resources with importlib + +1.6.0 + +* For developers: uses pyproject.toml instead of setup.py
* Fix progress bar fixing https://github.com/cokelaer/fitter/pull/74
* Fix BIC formula https://github.com/cokelaer/fitter/pull/77 -Installation -################### +1.5.2 -:: - git clone https://github.com/tg12/fitter - pip install . +* PR https://github.com/cokelaer/fitter/pull/74 to fix logger -**fitter** is also available on **conda** (bioconda channel):: +1.5.1 - conda install fitter +* Fixed regression putting back joblib +1.5.0 -Usage -################## +* Removed easydev and replaced by tqdm for progress bar
* Progressbar from tqdm also allows replacement of joblib need -standalone -=========== +1.4.1 -A standalone application (very simple) is also provided and works with input CSV -files:: +* Update timeout in docs from 10 to 30 seconds by @mpadge in https://github.com/cokelaer/fitter/pull/47
* Add Kolmogorov-Smirnov goodness-of-fit statistic by @lahdjirayhan in https://github.com/cokelaer/fitter/pull/58
* Switch branch from master to main - fitter fitdist data.csv --column-number 1 --distributions gamma,normal +1.4.0 -It creates a file called fitter.png and a log fitter.log +* get_best function now returns the parameters as a dictionary of parameter names and their values rather than just a list of values (https://github.com/cokelaer/fitter/issues/23) thanks to contributor @kabirmdasraful
* Accepting PR to fix progress bar issue reported in https://github.com/cokelaer/fitter/pull/37 -From Python shell -================== +1.3.0 -First, let us create a data samples with N = 10,000 points from a gamma distribution:: +* Parallel process implemented https://github.com/cokelaer/fitter/pull/25 thanks to @arsenyinfo - from scipy import stats - data = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) +1.2.3 -.. note:: the fitting is slow so keep the size value to reasonable value. +* Remove verbose arguments in Fitter class. Using the logging module instead
* The Fitter.fit has now a progress bar
* Add a standalone application called fitter (see the doc) -Now, without any knowledge about the distribution or its parameter, what is the distribution that fits the data best ? Scipy has 80 distributions and the **Fitter** class will scan all of them, call the fit function for you, ignoring those that fail or run forever and finally give you a summary of the best distributions in the sense of sum of the square errors. The best is to give an example:: +1.2.2 +Was not released - from fitter import Fitter - f = Fitter(data) - f.fit() - # may take some time since by default, all distributions are tried - # but you call manually provide a smaller set of distributions - f.summary() +1.2.1 +* Adding new class called histfit (see documentation) -.. image:: http://pythonhosted.org/fitter/_images/index-1.png - :target: http://pythonhosted.org/fitter/_images/index-1.png +1.2 +* Fixed the version. Previous version switched from 1.0.9 to 1.1.11. To start a fresh version, we increase to 1.2.0
* Merged pull request required by bioconda
* Merged pull request related to implementation of AIC/BIC/KL criteria (https://github.com/cokelaer/fitter/pull/19). This also fixes https://github.com/cokelaer/fitter/issues/9
* Implement two functions to get all distributions, or a list of common distributions to help users decreasing computational time (https://github.com/cokelaer/fitter/issues/20). Also added a FAQS section.
* Travis tested Python 3.6 and 3.7 (not 3.5 anymore) -See the `online `_ documentation for details. +1.1 +* Fixed deprecated warning
* Fitter is now in readthedocs at fitter.readthedocs.io -Contributors -============= +1.0.9 +* https://github.com/cokelaer/fitter/pull/8 and 11
* PR https://github.com/cokelaer/fitter/pull/8 -Setting up and maintaining Fitter has been possible thanks to users and contributors. -Thanks to all: +1.0.6 -.. image:: https://contrib.rocks/image?repo=cokelaer/fitter - :target: https://github.com/cokelaer/fitter/graphs/contributors +* summary() now returns the dataframe (instead of printing it) +1.0.5 +* https://github.com/cokelaer/fitter/issues +1.0.2 -Changelog -~~~~~~~~~ -========= ========================================================================== -Version Description -========= ========================================================================== -1.7.1 * integrate PR github.com/cokelaer/fitter/pull/100 from @vitorandreazza - to speedup multiprocessing run. -1.7.0 * replace logging with loguru - * main application update to add missing --output-image option and use - rich_click - * replace pkg_resources with importlib -1.6.0 * for developers: uses pyproject.toml instead of setup.py - * Fix progress bar fixing https://github.com/cokelaer/fitter/pull/74 - * Fix BIC formula https://github.com/cokelaer/fitter/pull/77 -1.5.2 * PR https://github.com/cokelaer/fitter/pull/74 to fix logger -1.5.1 * fixed regression putting back joblib -1.5.0 * removed easydev and replaced by tqdm for progress bar - * progressbar from tqdm also allows replacement of joblib need -1.4.1 * Update timeout in docs from 10 to 30 seconds by @mpadge in - https://github.com/cokelaer/fitter/pull/47 - * Add Kolmogorov-Smirnov goodness-of-fit statistic by @lahdjirayhan in - https://github.com/cokelaer/fitter/pull/58 - * switch branch from master to main -1.4.0 * get_best function now returns the parameters as a dictionary - of parameter names and their values rather than just a list of - values (https://github.com/cokelaer/fitter/issues/23) thanks to - contributor @kabirmdasraful - * Accepting PR to fix progress bar issue reported in - https://github.com/cokelaer/fitter/pull/37 -1.3.0 * parallel process implemented https://github.com/cokelaer/fitter/pull/25 - thanks to @arsenyinfo -1.2.3 * remove vervose arguments in Fitter class. Using the logging module - instead - * the Fitter.fit has now a progress bar - * add a standalone application called … fitter (see the doc) -1.2.2 was not released -1.2.1 adding new class called histfit (see documentation) -1.2 * Fixed the version. Previous version switched from - 1.0.9 to 1.1.11. To start a fresh version, we increase to 1.2.0 - * Merged pull request required by bioconda - * Merged pull request related to implementation of - AIC/BIC/KL criteria (https://github.com/cokelaer/fitter/pull/19). - This also fixes https://github.com/cokelaer/fitter/issues/9 - * Implement two functions to get all distributions, or a list of - common distributions to help users decreading computational time - (https://github.com/cokelaer/fitter/issues/20). Also added a FAQS - section. - * travis tested Python 3.6 and 3.7 (not 3.5 anymore) -1.1 * Fixed deprecated warning - * fitter is now in readthedocs at fitter.readthedocs.io -1.0.9 * https://github.com/cokelaer/fitter/pull/8 and 11 - PR https://github.com/cokelaer/fitter/pull/8 -1.0.6 * summary() now returns the dataframe (instead of printing it) -1.0.5 https://github.com/cokelaer/fitter/issues -1.0.2 add manifest to fix missing source in the pypi repository. -========= ========================================================================== +* Add manifest to fix missing source in the pypi repository. From 468a81bbf473085c1464f3cc336a9202f11ea55f Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:29:31 +0100 Subject: [PATCH 10/14] Update README.rst --- README.rst | 312 +++++++++++++++++++---------------------------------- 1 file changed, 110 insertions(+), 202 deletions(-) diff --git a/README.rst b/README.rst index c3680b1..39e1fdf 100644 --- a/README.rst +++ b/README.rst @@ -1,244 +1,152 @@ -############################# -FITTER Documentation -############################# - -.. image:: https://badge.fury.io/py/fitter.svg :target: https://pypi.python.org/pypi/fitter - -.. image:: https://github.com/cokelaer/fitter/actions/workflows/main.yml/badge.svg?branch=main :target: https://github.com/cokelaer/fitter/actions/workflows/main.yml - -.. image:: https://coveralls.io/repos/cokelaer/fitter/badge.png?branch=main :target: https://coveralls.io/r/cokelaer/fitter?branch=main - -.. image:: http://readthedocs.org/projects/fitter/badge/?version=latest :target: http://fitter.readthedocs.org/en/latest/?badge=latest :alt: Documentation Status - -.. image:: https://zenodo.org/badge/23078551.svg :target: https://zenodo.org/badge/latestdoi/23078551 - -**Compatible with Python 3.7, 3.8, 3.9, and 3.10** - -# What is it? - -The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. - -Using **fitter**, you can easily: - -- Fit a range of distributions to your data -- Compare their fit quality using multiple metrics (SSE, AIC, BIC, KL divergence) -- Identify the most suitable distribution for your dataset - -The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. - -# Installation - -You can install fitter in several ways: - -From GitHub:: - -``` -git clone https://github.com/tg12/fitter -cd fitter -pip install . - -``` - -From PyPI:: - -``` -pip install fitter - -``` - -From Conda (bioconda channel):: - -``` -conda install fitter - -``` - -# Usage - -## Standalone Application - -A standalone application is provided that works with input CSV files:: - -``` -fitter fitdist data.csv --column-number 1 --distributions gamma,normal - -``` - -This creates: -- A file called `fitter.png` with the visualization -- A log file `fitter.log` -## From Python Shell - -First, let's create a sample dataset with 10,000 points from a gamma distribution: - -```python -from scipy import stats -data = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) - -``` - -_Note: The fitting process can be computationally intensive, so keep the sample size reasonable._ - -Now, without any prior knowledge about the distribution or its parameters, we can use **Fitter** to find the best fit: - -```python -from fitter import Fitter -f = Fitter(data) -f.fit() -# This may take some time since by default, all distributions are tried -# You can manually provide a smaller set of distributions to speed up the process -f.summary() - -``` - -![Fitter Example](http://pythonhosted.org/fitter/_images/index-1.png) - -See the [online documentation](http://fitter.readthedocs.io/) for more details. - -# Running Tests - -To run the test suite for fitter, follow these steps: - -1. Clone the repository and install development dependencies: - -```bash -git clone https://github.com/tg12/fitter -cd fitter -pip3 install -e ".[dev]" - -``` - -2. Navigate to the tests directory: - -```bash -cd tests - -``` - -3. Run the test suite using the provided test runner: - -```bash -python3 run_tests.py - -``` - -### Additional Test Options - -The test runner supports several useful options: - -- Run with verbose output: - - ```bash - python3 run_tests.py -v - - ``` - -- Run a specific test file: - - ```bash - python3 run_tests.py -f test_fitter.py - - ``` - -- Run tests matching a specific keyword: - - ```bash - python3 run_tests.py -k "common" - - ``` - -- Specify an alternative test directory: - - ```bash - python3 run_tests.py -d ../other_tests - - ``` - - -The test runner automatically handles src directory structures and ensures proper import paths are configured. - -# Contributors - -Setting up and maintaining Fitter has been possible thanks to users and contributors. Thanks to all: +############################# +FITTER documentation +############################# -.. image:: https://contrib.rocks/image?repo=cokelaer/fitter :target: https://github.com/cokelaer/fitter/graphs/contributors +.. image:: https://badge.fury.io/py/fitter.svg + :target: https://pypi.python.org/pypi/fitter -# Changelog +.. image:: https://github.com/cokelaer/fitter/actions/workflows/main.yml/badge.svg?branch=main + :target: https://github.com/cokelaer/fitter/actions/workflows/main.yml -Version +.. image:: https://coveralls.io/repos/cokelaer/fitter/badge.png?branch=main + :target: https://coveralls.io/r/cokelaer/fitter?branch=main -Description +.. image:: http://readthedocs.org/projects/fitter/badge/?version=latest + :target: http://fitter.readthedocs.org/en/latest/?badge=latest + :alt: Documentation Status -1.7.1 +.. image:: https://zenodo.org/badge/23078551.svg + :target: https://zenodo.org/badge/latestdoi/23078551 -* Integrate PR github.com/cokelaer/fitter/pull/100 from @vitorandreazza to speedup multiprocessing run. +Compatible with Python 3.7, and 3.8, 3.9 -1.7.0 -* Replace logging with loguru
* Main application update to add missing --output-image option and use rich_click
* Replace pkg_resources with importlib +What is it ? +################ -1.6.0 +The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. -* For developers: uses pyproject.toml instead of setup.py
* Fix progress bar fixing https://github.com/cokelaer/fitter/pull/74
* Fix BIC formula https://github.com/cokelaer/fitter/pull/77 +Installation +################### -1.5.2 +:: -* PR https://github.com/cokelaer/fitter/pull/74 to fix logger + pip install fitter -1.5.1 +**fitter** is also available on **conda** (bioconda channel):: -* Fixed regression putting back joblib + conda install fitter -1.5.0 -* Removed easydev and replaced by tqdm for progress bar
* Progressbar from tqdm also allows replacement of joblib need +Usage +################## -1.4.1 +standalone +=========== -* Update timeout in docs from 10 to 30 seconds by @mpadge in https://github.com/cokelaer/fitter/pull/47
* Add Kolmogorov-Smirnov goodness-of-fit statistic by @lahdjirayhan in https://github.com/cokelaer/fitter/pull/58
* Switch branch from master to main +A standalone application (very simple) is also provided and works with input CSV +files:: -1.4.0 + fitter fitdist data.csv --column-number 1 --distributions gamma,normal -* get_best function now returns the parameters as a dictionary of parameter names and their values rather than just a list of values (https://github.com/cokelaer/fitter/issues/23) thanks to contributor @kabirmdasraful
* Accepting PR to fix progress bar issue reported in https://github.com/cokelaer/fitter/pull/37 +It creates a file called fitter.png and a log fitter.log -1.3.0 +From Python shell +================== -* Parallel process implemented https://github.com/cokelaer/fitter/pull/25 thanks to @arsenyinfo +First, let us create a data samples with N = 10,000 points from a gamma distribution:: -1.2.3 + from scipy import stats + data = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) -* Remove verbose arguments in Fitter class. Using the logging module instead
* The Fitter.fit has now a progress bar
* Add a standalone application called fitter (see the doc) +.. note:: the fitting is slow so keep the size value to reasonable value. -1.2.2 +Now, without any knowledge about the distribution or its parameter, what is the distribution that fits the data best ? Scipy has 80 distributions and the **Fitter** class will scan all of them, call the fit function for you, ignoring those that fail or run forever and finally give you a summary of the best distributions in the sense of sum of the square errors. The best is to give an example:: -Was not released -1.2.1 + from fitter import Fitter + f = Fitter(data) + f.fit() + # may take some time since by default, all distributions are tried + # but you call manually provide a smaller set of distributions + f.summary() -* Adding new class called histfit (see documentation) -1.2 +.. image:: http://pythonhosted.org/fitter/_images/index-1.png + :target: http://pythonhosted.org/fitter/_images/index-1.png -* Fixed the version. Previous version switched from 1.0.9 to 1.1.11. To start a fresh version, we increase to 1.2.0
* Merged pull request required by bioconda
* Merged pull request related to implementation of AIC/BIC/KL criteria (https://github.com/cokelaer/fitter/pull/19). This also fixes https://github.com/cokelaer/fitter/issues/9
* Implement two functions to get all distributions, or a list of common distributions to help users decreasing computational time (https://github.com/cokelaer/fitter/issues/20). Also added a FAQS section.
* Travis tested Python 3.6 and 3.7 (not 3.5 anymore) -1.1 +See the `online `_ documentation for details. -* Fixed deprecated warning
* Fitter is now in readthedocs at fitter.readthedocs.io -1.0.9 +Contributors +============= -* https://github.com/cokelaer/fitter/pull/8 and 11
* PR https://github.com/cokelaer/fitter/pull/8 -1.0.6 +Setting up and maintaining Fitter has been possible thanks to users and contributors. +Thanks to all: -* summary() now returns the dataframe (instead of printing it) +.. image:: https://contrib.rocks/image?repo=cokelaer/fitter + :target: https://github.com/cokelaer/fitter/graphs/contributors -1.0.5 -* https://github.com/cokelaer/fitter/issues -1.0.2 -* Add manifest to fix missing source in the pypi repository. +Changelog +~~~~~~~~~ +========= ========================================================================== +Version Description +========= ========================================================================== +1.7.1 * integrate PR github.com/cokelaer/fitter/pull/100 from @vitorandreazza + to speedup multiprocessing run. +1.7.0 * replace logging with loguru + * main application update to add missing --output-image option and use + rich_click + * replace pkg_resources with importlib +1.6.0 * for developers: uses pyproject.toml instead of setup.py + * Fix progress bar fixing https://github.com/cokelaer/fitter/pull/74 + * Fix BIC formula https://github.com/cokelaer/fitter/pull/77 +1.5.2 * PR https://github.com/cokelaer/fitter/pull/74 to fix logger +1.5.1 * fixed regression putting back joblib +1.5.0 * removed easydev and replaced by tqdm for progress bar + * progressbar from tqdm also allows replacement of joblib need +1.4.1 * Update timeout in docs from 10 to 30 seconds by @mpadge in + https://github.com/cokelaer/fitter/pull/47 + * Add Kolmogorov-Smirnov goodness-of-fit statistic by @lahdjirayhan in + https://github.com/cokelaer/fitter/pull/58 + * switch branch from master to main +1.4.0 * get_best function now returns the parameters as a dictionary + of parameter names and their values rather than just a list of + values (https://github.com/cokelaer/fitter/issues/23) thanks to + contributor @kabirmdasraful + * Accepting PR to fix progress bar issue reported in + https://github.com/cokelaer/fitter/pull/37 +1.3.0 * parallel process implemented https://github.com/cokelaer/fitter/pull/25 + thanks to @arsenyinfo +1.2.3 * remove vervose arguments in Fitter class. Using the logging module + instead + * the Fitter.fit has now a progress bar + * add a standalone application called … fitter (see the doc) +1.2.2 was not released +1.2.1 adding new class called histfit (see documentation) +1.2 * Fixed the version. Previous version switched from + 1.0.9 to 1.1.11. To start a fresh version, we increase to 1.2.0 + * Merged pull request required by bioconda + * Merged pull request related to implementation of + AIC/BIC/KL criteria (https://github.com/cokelaer/fitter/pull/19). + This also fixes https://github.com/cokelaer/fitter/issues/9 + * Implement two functions to get all distributions, or a list of + common distributions to help users decreading computational time + (https://github.com/cokelaer/fitter/issues/20). Also added a FAQS + section. + * travis tested Python 3.6 and 3.7 (not 3.5 anymore) +1.1 * Fixed deprecated warning + * fitter is now in readthedocs at fitter.readthedocs.io +1.0.9 * https://github.com/cokelaer/fitter/pull/8 and 11 + PR https://github.com/cokelaer/fitter/pull/8 +1.0.6 * summary() now returns the dataframe (instead of printing it) +1.0.5 https://github.com/cokelaer/fitter/issues +1.0.2 add manifest to fix missing source in the pypi repository. +========= ========================================================================== From a5e53a3d7d3d329f41f807162166792551cd0456 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:30:27 +0100 Subject: [PATCH 11/14] Update README.rst --- README.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 39e1fdf..9e08547 100644 --- a/README.rst +++ b/README.rst @@ -33,12 +33,9 @@ Installation :: - pip install fitter - -**fitter** is also available on **conda** (bioconda channel):: - - conda install fitter - + git clone https://github.com/tg12/fitter + cd fitter + pip install . Usage ################## From fd28b88ea5c40da2ba3bdce77b156ab7072ddac8 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:31:40 +0100 Subject: [PATCH 12/14] Update README.rst --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 9e08547..3d72938 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,18 @@ Installation cd fitter pip install . +Run Tests +################### + +:: + + git clone https://github.com/tg12/fitter + cd fitter + pip3 install -e ".[dev]" + cd tests + python3 run_tests.py -v + + Usage ################## From 3ec4d84823349a29cace5a2f06aa1e7072375c83 Mon Sep 17 00:00:00 2001 From: tg12 Date: Wed, 2 Apr 2025 15:33:13 +0100 Subject: [PATCH 13/14] Update README.rst --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 3d72938..d0d0bea 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,8 @@ What is it ? The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. +**I'm deeply appreciative of the excellent work done by the original creator of fitter (https://github.com/cokelaer/fitter). Their thoughtful implementation of distribution fitting algorithms provided a solid foundation that inspired me to create this fork. While maintaining full respect for their valuable contribution, I've enhanced the package with several key improvements including better NumPy 2.0 compatibility, comprehensive test coverage with an improved test runner, optimized parallel processing for faster distribution fitting, and cleaner code following PEP 8 standards. These quality-of-life improvements aim to extend the package's utility while preserving the brilliant core functionality and intuitive design that made the original so valuable to the data science community. All credit for the original concept and implementation remains with the original author.** + Installation ################### From 812dfb19693a751278224c3dc8ad15ef3dffed49 Mon Sep 17 00:00:00 2001 From: James Sawyer Date: Sat, 18 Oct 2025 23:00:21 +0100 Subject: [PATCH 14/14] code refactor --- .gitignore | 79 ++- .pre-commit-config.yaml | 33 + .ruff.toml | 48 ++ README.rst | 17 +- poetry.lock | 1385 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 92 ++- requirements.txt | 17 +- src/fitter/fitter.py | 589 ++++++++++------- src/fitter/histfit.py | 303 ++++++--- src/fitter/main.py | 205 ++++-- test/test_main.py | 3 +- 11 files changed, 2278 insertions(+), 493 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 .ruff.toml create mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index 1800114..4456285 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ + # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -27,8 +28,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -46,7 +47,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ cover/ @@ -92,31 +93,38 @@ ipython_config.py # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +# uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# poetry.lock +# poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -124,11 +132,25 @@ __pypackages__/ celerybeat-schedule celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -161,14 +183,35 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ # Ruff stuff: .ruff_cache/ # PyPI configuration file -.pypirc \ No newline at end of file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6707eac --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,33 @@ + +files: '\.(py|rst|sh)$' +fail_fast: false + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + #- id: check-executables-have-shebangs + - id: check-ast + +- repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + args: ["-j8", "--ignore=E203,E501,W503,E722", "--max-line-length=120", "--exit-zero"] + +- repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black + args: ["--line-length=120"] + exclude: E501 + +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] # solves conflicts between black and isort + diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..0142ebf --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,48 @@ +# black formatter takes care of the line length +line-length = 999 + +lint.select = ["ALL"] +fixable = ["ALL"] + +lint.ignore = [ + "UP006", # https://github.com/charliermarsh/ruff/pull/4427 + "UP007", # https://github.com/charliermarsh/ruff/pull/4427 + # Mutable class attributes should be annotated with `typing.ClassVar` + # Too many violations + "RUF012", + # Logging statement uses f-string + "G004", + "T201", # flake8-print + "ERA001", # Commented out code + "W291", # trailing whitespace + "UP018" # native-literals (UP018). +] + +# Mininal python version we support is 3.13 +target-version = "py313" + +[lint.per-file-ignores] +# python scripts in bin/ needs some python path configurations before import +"bin/*.py" = [ + # E402: module-import-not-at-top-of-file + "E402", + # S603: `subprocess` call: check for execution of untrusted input + # these are dev tools and do not have risks of malicious inputs. + "S603", + # T201 `print` found + # print() is allowed in bin/ as they are dev tools. + "T201", +] + +[lint.pylint] +max-args = 6 # We have many functions reaching 6 args + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Enable preview features. +preview = true + +[analyze] +detect-string-imports = true \ No newline at end of file diff --git a/README.rst b/README.rst index d0d0bea..39e1fdf 100644 --- a/README.rst +++ b/README.rst @@ -28,27 +28,16 @@ What is it ? The **fitter** package is a Python library used for fitting probability distributions to data. It provides a straightforward and and intuitive interface to estimate parameters for various types of distributions, both continuous and discrete. Using **fitter**, you can easily fit a range of distributions to your data and compare their fit, aiding in the selection of the most suitable distribution. The package is designed to be user-friendly and requires minimal setup, making it a useful tool for data scientists and statisticians working with probability distributions. -**I'm deeply appreciative of the excellent work done by the original creator of fitter (https://github.com/cokelaer/fitter). Their thoughtful implementation of distribution fitting algorithms provided a solid foundation that inspired me to create this fork. While maintaining full respect for their valuable contribution, I've enhanced the package with several key improvements including better NumPy 2.0 compatibility, comprehensive test coverage with an improved test runner, optimized parallel processing for faster distribution fitting, and cleaner code following PEP 8 standards. These quality-of-life improvements aim to extend the package's utility while preserving the brilliant core functionality and intuitive design that made the original so valuable to the data science community. All credit for the original concept and implementation remains with the original author.** - Installation ################### :: - git clone https://github.com/tg12/fitter - cd fitter - pip install . - -Run Tests -################### + pip install fitter -:: +**fitter** is also available on **conda** (bioconda channel):: - git clone https://github.com/tg12/fitter - cd fitter - pip3 install -e ".[dev]" - cd tests - python3 run_tests.py -v + conda install fitter Usage diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..ce0fa7c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1385 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +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"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "contourpy" +version = "1.1.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, + {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, + {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, + {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, + {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, + {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, + {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, + {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, + {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, + {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, + {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, + {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, + {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, + {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, + {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, + {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, + {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, + {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, + {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, + {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, + {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, +] + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + +[[package]] +name = "contourpy" +version = "1.1.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.12\"" +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\""} + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "coveralls" +version = "3.3.1" +description = "Show coverage stats online via coveralls.io" +optional = false +python-versions = ">= 3.5" +groups = ["dev"] +files = [ + {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, + {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, +] + +[package.dependencies] +coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10)"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main"] +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 = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.10\"" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "fonttools" +version = "4.47.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2404107626f97a221dc1a65b05396d2bb2ce38e435f64f26ed2369f68675d9"}, + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01f409be619a9a0f5590389e37ccb58b47264939f0e8d58bfa1f3ba07d22671"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d986b66ff722ef675b7ee22fbe5947a41f60a61a4da15579d5e276d897fbc7fa"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8acf6dd0434b211b3bd30d572d9e019831aae17a54016629fa8224783b22df8"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:495369c660e0c27233e3c572269cbe520f7f4978be675f990f4005937337d391"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59227d7ba5b232281c26ae04fac2c73a79ad0e236bca5c44aae904a18f14faf"}, + {file = "fonttools-4.47.0-cp310-cp310-win32.whl", hash = "sha256:59a6c8b71a245800e923cb684a2dc0eac19c56493e2f896218fcf2571ed28984"}, + {file = "fonttools-4.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:52c82df66201f3a90db438d9d7b337c7c98139de598d0728fb99dab9fd0495ca"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:854421e328d47d70aa5abceacbe8eef231961b162c71cbe7ff3f47e235e2e5c5"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:511482df31cfea9f697930f61520f6541185fa5eeba2fa760fe72e8eee5af88b"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0e2c88c8c985b7b9a7efcd06511fb0a1fe3ddd9a6cd2895ef1dbf9059719d7"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7a0a8848726956e9d9fb18c977a279013daadf0cbb6725d2015a6dd57527992"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e869da810ae35afb3019baa0d0306cdbab4760a54909c89ad8904fa629991812"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd23848f877c3754f53a4903fb7a593ed100924f9b4bff7d5a4e2e8a7001ae11"}, + {file = "fonttools-4.47.0-cp311-cp311-win32.whl", hash = "sha256:bf1810635c00f7c45d93085611c995fc130009cec5abdc35b327156aa191f982"}, + {file = "fonttools-4.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:61df4dee5d38ab65b26da8efd62d859a1eef7a34dcbc331299a28e24d04c59a7"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e3f4d61f3a8195eac784f1d0c16c0a3105382c1b9a74d99ac4ba421da39a8826"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:174995f7b057e799355b393e97f4f93ef1f2197cbfa945e988d49b2a09ecbce8"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea592e6a09b71cb7a7661dd93ac0b877a6228e2d677ebacbad0a4d118494c86d"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bdbe90b33897d9cc4a39f8e415b0fcdeae4c40a99374b8a4982f127ff5c767"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:843509ae9b93db5aaf1a6302085e30bddc1111d31e11d724584818f5b698f500"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9acfa1cdc479e0dde528b61423855913d949a7f7fe09e276228298fef4589540"}, + {file = "fonttools-4.47.0-cp312-cp312-win32.whl", hash = "sha256:66c92ec7f95fd9732550ebedefcd190a8d81beaa97e89d523a0d17198a8bda4d"}, + {file = "fonttools-4.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8fa20748de55d0021f83754b371432dca0439e02847962fc4c42a0e444c2d78"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c75e19971209fbbce891ebfd1b10c37320a5a28e8d438861c21d35305aedb81c"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e79f1a3970d25f692bbb8c8c2637e621a66c0d60c109ab48d4a160f50856deff"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:562681188c62c024fe2c611b32e08b8de2afa00c0c4e72bed47c47c318e16d5c"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a77a60315c33393b2bd29d538d1ef026060a63d3a49a9233b779261bad9c3f71"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4fabb8cc9422efae1a925160083fdcbab8fdc96a8483441eb7457235df625bd"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a78dba8c2a1e9d53a0fb5382979f024200dc86adc46a56cbb668a2249862fda"}, + {file = "fonttools-4.47.0-cp38-cp38-win32.whl", hash = "sha256:e6b968543fde4119231c12c2a953dcf83349590ca631ba8216a8edf9cd4d36a9"}, + {file = "fonttools-4.47.0-cp38-cp38-win_amd64.whl", hash = "sha256:4a9a51745c0439516d947480d4d884fa18bd1458e05b829e482b9269afa655bc"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:62d8ddb058b8e87018e5dc26f3258e2c30daad4c87262dfeb0e2617dd84750e6"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dde0eab40faaa5476133123f6a622a1cc3ac9b7af45d65690870620323308b4"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4da089f6dfdb822293bde576916492cd708c37c2501c3651adde39804630538"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:253bb46bab970e8aae254cebf2ae3db98a4ef6bd034707aa68a239027d2b198d"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1193fb090061efa2f9e2d8d743ae9850c77b66746a3b32792324cdce65784154"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:084511482dd265bce6dca24c509894062f0117e4e6869384d853f46c0e6d43be"}, + {file = "fonttools-4.47.0-cp39-cp39-win32.whl", hash = "sha256:97620c4af36e4c849e52661492e31dc36916df12571cb900d16960ab8e92a980"}, + {file = "fonttools-4.47.0-cp39-cp39-win_amd64.whl", hash = "sha256:e77bdf52185bdaf63d39f3e1ac3212e6cfa3ab07d509b94557a8902ce9c13c82"}, + {file = "fonttools-4.47.0-py3-none-any.whl", hash = "sha256:d6477ba902dd2d7adda7f0fd3bfaeb92885d45993c9e1928c9f28fc3961415f7"}, + {file = "fonttools-4.47.0.tar.gz", hash = "sha256:ec13a10715eef0e031858c1c23bfaee6cba02b97558e4a7bfa089dba4a8c2ebf"}, +] + +[package.extras] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr ; sys_platform == \"darwin\""] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-resources" +version = "6.1.1" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.10\"" +files = [ + {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, + {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +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 = "joblib" +version = "1.3.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, + {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5) ; python_version >= \"3.9\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "mypy (==v1.5.1) ; python_version >= \"3.8\"", "pre-commit (==3.4.0) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.4.0) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.1.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.0.0) ; python_version >= \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.9\"", "sphinx-rtd-theme (==1.3.0) ; python_version >= \"3.9\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.11.0) ; python_version >= \"3.8\""] + +[[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" +groups = ["main"] +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 = "matplotlib" +version = "3.7.4" +description = "Python plotting package" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "matplotlib-3.7.4-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:b71079239bd866bf56df023e5146de159cb0c7294e508830901f4d79e2d89385"}, + {file = "matplotlib-3.7.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bf91a42f6274a64cb41189120b620c02e574535ff6671fa836cade7701b06fbd"}, + {file = "matplotlib-3.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f757e8b42841d6add0cb69b42497667f0d25a404dcd50bd923ec9904e38414c4"}, + {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dfee00aa4bd291e08bb9461831c26ce0da85ca9781bb8794f2025c6e925281"}, + {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3640f33632beb3993b698b1be9d1c262b742761d6101f3c27b87b2185d25c875"}, + {file = "matplotlib-3.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff539c4a17ecdf076ed808ee271ffae4a30dcb7e157b99ccae2c837262c07db6"}, + {file = "matplotlib-3.7.4-cp310-cp310-win32.whl", hash = "sha256:24b8f28af3e766195c09b780b15aa9f6710192b415ae7866b9c03dee7ec86370"}, + {file = "matplotlib-3.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fa193286712c3b6c3cfa5fe8a6bb563f8c52cc750006c782296e0807ce5e799"}, + {file = "matplotlib-3.7.4-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:b167f54cb4654b210c9624ec7b54e2b3b8de68c93a14668937e7e53df60770ec"}, + {file = "matplotlib-3.7.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7dfe6821f1944cb35603ff22e21510941bbcce7ccf96095beffaac890d39ce77"}, + {file = "matplotlib-3.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3c557d9165320dff3c5f2bb99bfa0b6813d3e626423ff71c40d6bc23b83c3339"}, + {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08372696b3bb45c563472a552a705bfa0942f0a8ffe084db8a4e8f9153fbdf9d"}, + {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81e1a7ac818000e8ac3ca696c3fdc501bc2d3adc89005e7b4e22ee5e9d51de98"}, + {file = "matplotlib-3.7.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390920a3949906bc4b0216198d378f2a640c36c622e3584dd0c79a7c59ae9f50"}, + {file = "matplotlib-3.7.4-cp311-cp311-win32.whl", hash = "sha256:62e094d8da26294634da9e7f1856beee3978752b1b530c8e1763d2faed60cc10"}, + {file = "matplotlib-3.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:f8fc2df756105784e650605e024d36dc2d048d68e5c1b26df97ee25d1bd41f9f"}, + {file = "matplotlib-3.7.4-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:568574756127791903604e315c11aef9f255151e4cfe20ec603a70f9dda8e259"}, + {file = "matplotlib-3.7.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7d479aac338195e2199a8cfc03c4f2f55914e6a120177edae79e0340a6406457"}, + {file = "matplotlib-3.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32183d4be84189a4c52b4b8861434d427d9118db2cec32986f98ed6c02dcfbb6"}, + {file = "matplotlib-3.7.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0037d066cca1f4bda626c507cddeb6f7da8283bc6a214da2db13ff2162933c52"}, + {file = "matplotlib-3.7.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44856632ebce88abd8efdc0a0dceec600418dcac06b72ae77af0019d260aa243"}, + {file = "matplotlib-3.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:632fc938c22117d4241411191cfb88ac264a4c0a9ac702244641ddf30f0d739c"}, + {file = "matplotlib-3.7.4-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:ce163be048613b9d1962273708cc97e09ca05d37312e670d166cf332b80bbaff"}, + {file = "matplotlib-3.7.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e680f49bb8052ba3b2698e370155d2b4afb49f9af1cc611a26579d5981e2852a"}, + {file = "matplotlib-3.7.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0604880e4327114054199108b7390f987f4f40ee5ce728985836889e11a780ba"}, + {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1e6abcde6fc52475f9d6a12b9f1792aee171ce7818ef6df5d61cb0b82816e6e8"}, + {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f59a70e2ec3212033ef6633ed07682da03f5249379722512a3a2a26a7d9a738e"}, + {file = "matplotlib-3.7.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a9981b2a2dd9da06eca4ab5855d09b54b8ce7377c3e0e3957767b83219d652d"}, + {file = "matplotlib-3.7.4-cp38-cp38-win32.whl", hash = "sha256:83859ac26839660ecd164ee8311272074250b915ac300f9b2eccc84410f8953b"}, + {file = "matplotlib-3.7.4-cp38-cp38-win_amd64.whl", hash = "sha256:7a7709796ac59fe8debde68272388be6ed449c8971362eb5b60d280eac8dadde"}, + {file = "matplotlib-3.7.4-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:b1d70bc1ea1bf110bec64f4578de3e14947909a8887df4c1fd44492eca487955"}, + {file = "matplotlib-3.7.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c83f49e795a5de6c168876eea723f5b88355202f9603c55977f5356213aa8280"}, + {file = "matplotlib-3.7.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c9133f230945fe10652eb33e43642e933896194ef6a4f8d5e79bb722bdb2000"}, + {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798ff59022eeb276380ce9a73ba35d13c3d1499ab9b73d194fd07f1b0a41c304"}, + {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1707b20b25e90538c2ce8d4409e30f0ef1df4017cc65ad0439633492a973635b"}, + {file = "matplotlib-3.7.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e6227ca8492baeef873cdd8e169a318efb5c3a25ce94e69727e7f964995b0b1"}, + {file = "matplotlib-3.7.4-cp39-cp39-win32.whl", hash = "sha256:5661c8639aded7d1bbf781373a359011cb1dd09199dee49043e9e68dd16f07ba"}, + {file = "matplotlib-3.7.4-cp39-cp39-win_amd64.whl", hash = "sha256:55eec941a4743f0bd3e5b8ee180e36b7ea8e62f867bf2613937c9f01b9ac06a2"}, + {file = "matplotlib-3.7.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab16868714e5cc90ec8f7ff5d83d23bcd6559224d8e9cb5227c9f58748889fe8"}, + {file = "matplotlib-3.7.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c698b33f9a3f0b127a8e614c8fb4087563bb3caa9c9d95298722fa2400cdd3f"}, + {file = "matplotlib-3.7.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be3493bbcb4d255cb71de1f9050ac71682fce21a56089eadbcc8e21784cb12ee"}, + {file = "matplotlib-3.7.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f8c725d1dd2901b2e7ec6cd64165e00da2978cc23d4143cb9ef745bec88e6b04"}, + {file = "matplotlib-3.7.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:286332f8f45f8ffde2d2119b9fdd42153dccd5025fa9f451b4a3b5c086e26da5"}, + {file = "matplotlib-3.7.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:116ef0b43aa00ff69260b4cce39c571e4b8c6f893795b708303fa27d9b9d7548"}, + {file = "matplotlib-3.7.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90590d4b46458677d80bc3218f3f1ac11fc122baa9134e0cb5b3e8fc3714052"}, + {file = "matplotlib-3.7.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de7c07069687be64fd9d119da3122ba13a8d399eccd3f844815f0dc78a870b2c"}, + {file = "matplotlib-3.7.4.tar.gz", hash = "sha256:7cd4fef8187d1dd0d9dcfdbaa06ac326d396fb8c71c647129f0bf56835d77026"}, +] + +[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.0.1" +numpy = ">=1.20,<2" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +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 = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +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"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandas" +version = "2.0.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.8" +groups = ["main"] +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"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version == \"3.10\""}, +] +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)"] + +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +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"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions ; python_version < \"3.10\""] +xmp = ["defusedxml"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata ; python_version < \"3.8\""] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +groups = ["main"] +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-timeout" +version = "1.4.2" +description = "py.test plugin to abort hanging tests" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"}, + {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"}, +] + +[package.dependencies] +pytest = ">=3.6.0" + +[[package]] +name = "pytest-xdist" +version = "3.5.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +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"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[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 = "rich" +version = "13.7.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, +] + +[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)"] + +[[package]] +name = "rich-click" +version = "1.7.2" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "rich-click-1.7.2.tar.gz", hash = "sha256:22f93439a3d65f4a04e07cd584f4d01d132d96899766af92ed287618156abbe2"}, + {file = "rich_click-1.7.2-py3-none-any.whl", hash = "sha256:a42bcdcb8696c4ca7a3b1a39e1aba3d2cb64ad00690b4c022fdcb2cbccebc3fc"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7.0" +typing-extensions = "*" + +[package.extras] +dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] + +[[package]] +name = "scipy" +version = "1.9.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, + {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, + {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, + {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, + {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, + {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, + {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, +] + +[package.dependencies] +numpy = ">=1.18.5,<1.26.0" + +[package.extras] +dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] +test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.10\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tqdm" +version = "4.66.3" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"}, + {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "tzdata" +version = "2023.4" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + +[[package]] +name = "zipp" +version = "3.19.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.10\"" +files = [ + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "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)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.8" +content-hash = "64b630ca97680821cc0723f0d9f6714ab4fc3386692729fa42f1a41b4c854aba" diff --git a/pyproject.toml b/pyproject.toml index ae7e6c9..9e2f0b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,73 +1,55 @@ [tool.poetry] name = "fitter" -version = "1.8.0" # Bumped version for numpy 2 compatibility +version = "1.7.1" description = "A tool to fit data to many distributions and get the best one(s)" authors = ["Thomas Cokelaer "] license = "GPL" readme = "README.rst" -repository = "https://github.com/cokelaer/fitter" -documentation = "https://fitter.readthedocs.io" -keywords = ["fit", "distribution", "fitting", "scipy", "statistics"] +keywords = ["fit", "distribution", "fitting", "scipy"] classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Education", - "Intended Audience :: End Users/Desktop", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Scientific/Engineering :: Bio-Informatics", - "Topic :: Scientific/Engineering :: Information Analysis", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Information Analysis", ] -[tool.poetry.dependencies] -python = ">=3.8,<4.0" -numpy = ">=2.0.0" -scipy = ">=1.10.0" -matplotlib = ">=3.5.0" -pandas = ">=1.3.0" -pytest = {version = ">=7.0.0", optional = true} - -[tool.poetry.group.dev.dependencies] -pytest = ">=7.0.0" -pytest-cov = ">=4.0.0" -black = ">=23.0.0" -isort = ">=5.12.0" -mypy = ">=1.0.0" -pre-commit = ">=3.0.0" - -[tool.poetry.extras] -test = ["pytest", "pytest-cov"] -[tool.isort] -profile = "black" -line_length = 88 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -[tool.black] -line-length = 88 -target-version = ["py38", "py39", "py310", "py311", "py312"] +[tool.poetry.dependencies] +python = "^3.8" +click = "^8.3.0" +joblib = "^1.5.2" +matplotlib = "^3.10.7" +numpy = "^2.3.4" +pandas = "^2.3.3" +scipy = "^1.16.2" +tqdm = "^4.67.1" +loguru = "^0.7.3" +rich-click = "^1.9.3" -[tool.mypy] -python_version = "3.8" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = false -disallow_incomplete_defs = false +[tool.poetry.group.dev.dependencies] +pytest = "^8.4.2" +pytest-cov = "^7.0.0" +pytest-xdist = "^3.8.0" +pytest-mock = "^3.15.1" +pytest-timeout = "^2.4.0" +coveralls = "^3.3.1" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] fitter = "fitter.main:main" + + diff --git a/requirements.txt b/requirements.txt index 5c1b1de..8073962 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,12 @@ -click>=8.1.8 -joblib>=1.4.2 +click>=8.3.0 +joblib>=1.5.2 loguru>=0.7.3 -numba>=0.61.0rc2 -numpy>=2.2.4 -pandas>=2.2.3 -pytest>=8.3.5 -rich_click>=1.8.8 -scipy>=1.15.2 +matplotlib>=3.10.7 +numpy>=2.3.4 +pandas>=2.3.3 +pytest>=8.4.2 +rich_click>=1.9.3 +scipy>=1.16.2 +setuptools>=80.9.0 Sphinx>=8.2.3 tqdm>=4.67.1 diff --git a/src/fitter/fitter.py b/src/fitter/fitter.py index 69f5bad..552a83a 100644 --- a/src/fitter/fitter.py +++ b/src/fitter/fitter.py @@ -13,23 +13,27 @@ # Package: http://pypi.python.org/fitter # ############################################################################## -"""main module of the fitter package +"""Main module of the fitter package. -.. sectionauthor:: Thomas Cokelaer, Aug 2014-2020 +This module provides the Fitter class for fitting multiple probability distributions +to data samples and comparing their goodness of fit using various metrics. +.. sectionauthor:: Thomas Cokelaer, Aug 2014-2020 """ +from __future__ import annotations + import contextlib import multiprocessing +from typing import Any import joblib import numpy as np import pandas as pd -import pylab import scipy.stats from joblib.parallel import Parallel, delayed from loguru import logger -from numba import jit, prange +from matplotlib import pyplot as plt from scipy.stats import entropy as kl_div from scipy.stats import kstest from tqdm import tqdm @@ -41,100 +45,66 @@ # https://stackoverflow.com/questions/24983493/tracking-progress-of-joblib-parallel-execution/58936697#58936697 # and https://github.com/louisabraham/tqdm_joblib @contextlib.contextmanager -def tqdm_joblib(*args, **kwargs): - """Context manager to patch joblib to report into tqdm progress bar - given as argument +def tqdm_joblib(*args: Any, **kwargs: Any) -> Any: + """Context manager to patch joblib to report into tqdm progress bar. + + Args: + *args: Positional arguments passed to tqdm. + **kwargs: Keyword arguments passed to tqdm. + + Yields: + tqdm object for progress tracking. + """ - # Only create progress bar if not disabled (saves overhead when progress tracking is off) - disable = kwargs.get('disable', False) - tqdm_object = tqdm(*args, **kwargs) if not disable else None - - if not disable: - class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): - def __call__(self, *args, **kwargs): - tqdm_object.update(n=self.batch_size) - return super().__call__(*args, **kwargs) - - old_batch_callback = joblib.parallel.BatchCompletionCallBack - joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback - + tqdm_object = tqdm(*args, **kwargs) + + class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __call__(self, *args, **kwargs): + tqdm_object.update(n=self.batch_size) + return super().__call__(*args, **kwargs) + + old_batch_callback = joblib.parallel.BatchCompletionCallBack + joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback try: yield tqdm_object finally: - if not disable: - joblib.parallel.BatchCompletionCallBack = old_batch_callback - tqdm_object.close() + joblib.parallel.BatchCompletionCallBack = old_batch_callback + tqdm_object.close() -def get_distributions(verbose=False): - """ - Discover all available distributions in scipy.stats that have a 'fit' method. - - Parameters: - ----------- - verbose : bool - Whether to print discovery information during execution - +def get_distributions() -> list[str]: + """Get all scipy.stats distributions that have a fit method. + Returns: - -------- - list - List of distribution names that have a 'fit' method + List of distribution names as strings. + """ - - - distributions = [] - skipped = [] - - if verbose: - logger.info("Searching for distributions in scipy.stats with a 'fit' method...") - - for dist_name in dir(scipy.stats): - try: - # Skip private attributes, functions, and non-distribution objects - if dist_name.startswith('_') or dist_name in ['test', 'freeze']: - continue - - # Get the attribute - dist_obj = getattr(scipy.stats, dist_name) - - # Check if it's a distribution-like object with a fit method - if hasattr(dist_obj, 'fit'): - distributions.append(dist_name) - if verbose: - logger.debug(f"Found distribution: {dist_name}") - else: - skipped.append(dist_name) - - except Exception as e: - # Safely handle any unexpected errors during discovery - skipped.append(dist_name) - if verbose: - logger.warning(f"Error checking {dist_name}: {str(e)}") - - if verbose: - logger.success(f"Found {len(distributions)} distributions with a 'fit' method") - + # BUGFIX: Replace eval() with getattr() - safer and faster + distributions = [ + name for name in dir(scipy.stats) + if hasattr(getattr(scipy.stats, name, None), 'fit') + ] return distributions -def get_common_distributions(verbose=False): - """ - Get a curated list of common distributions that have a 'fit' method. - - Parameters: - ----------- - verbose : bool - Whether to print information during execution - +def get_common_distributions() -> list[str]: + """Get commonly used distributions that are available in scipy.stats. + Returns: - -------- - list - List of common distribution names that have a 'fit' method - """ - from loguru import logger + List of common distribution names that have fit methods. - # Define the list of common distributions - common_distributions = [ + Note: + Filters based on scipy version to avoid errors with missing distributions. + + """ + distributions = get_distributions() + # Convert to set for O(1) lookup (much faster than list membership) + dist_set = set(distributions) + # Common distributions to avoid error due to changes in scipy + common = [ "cauchy", "chi2", "expon", @@ -146,20 +116,8 @@ def get_common_distributions(verbose=False): "rayleigh", "uniform", ] - - # Get all available distributions - all_distributions = get_distributions(verbose=False) - - # Filter to only include distributions that exist in scipy.stats - available_common = [dist for dist in common_distributions if dist in all_distributions] - - if verbose: - logger.info(f"Found {len(available_common)} common distributions out of {len(all_distributions)} total") - if len(available_common) < len(common_distributions): - missing = set(common_distributions) - set(available_common) - logger.warning(f"Missing common distributions: {', '.join(missing)}") - - return available_common + # Single-pass filter with set lookup (O(n) instead of O(n²)) + return [x for x in common if x in dist_set] class Fitter: @@ -228,14 +186,14 @@ class Fitter: def __init__( self, - data, - xmin=None, - xmax=None, - bins=100, - distributions=None, - timeout=30, - density=True, - ): + data: np.ndarray | list[float], + xmin: float | None = None, + xmax: float | None = None, + bins: int = 100, + distributions: list[str] | str | None = None, + timeout: int = 30, + density: bool = True, + ) -> None: """.. rubric:: Constructor :param list data: a numpy array or a list @@ -270,55 +228,67 @@ def __init__( self._density = True #: list of distributions to test - self.distributions = distributions - if self.distributions is None: + self.distributions: list[str] + if distributions is None: self._load_all_distributions() - elif self.distributions == "common": + elif distributions == "common": self.distributions = get_common_distributions() elif isinstance(distributions, str): self.distributions = [distributions] + else: + self.distributions = distributions self.bins = bins - self._alldata = np.array(data) - if xmin is None: - self._xmin = self._alldata.min() - else: - self._xmin = xmin - if xmax is None: - self._xmax = self._alldata.max() - else: - self._xmax = xmax + self._alldata: np.ndarray = np.asarray(data) + # Use ternary for cleaner code + self._xmin: float = self._alldata.min() if xmin is None else xmin + self._xmax: float = self._alldata.max() if xmax is None else xmax self._trim_data() self._update_data_pdf() - self._init() # Other attributes - - def _init(self): - self.fitted_param = {} - self.fitted_pdf = {} - self._fitted_errors = {} - self._aic = {} - self._bic = {} - self._kldiv = {} - self._ks_stat = {} - self._ks_pval = {} - self._fit_i = 0 # fit progress - # self.pb = None - - def _update_data_pdf(self): - # histogram retuns X with N+1 values. So, we rearrange the X output into only N - self.y, self.x = np.histogram(self._data, bins=self.bins, density=self._density) - self.x = [(this + self.x[i + 1]) / 2.0 for i, this in enumerate(self.x[0:-1])] - - def _trim_data(self): - self._data = self._alldata[np.logical_and(self._alldata >= self._xmin, self._alldata <= self._xmax)] - - def _get_xmin(self): + # Other attributes + self._init() + + def _init(self) -> None: + """Initialize result storage dictionaries.""" + self.fitted_param: dict[str, tuple] = {} + self.fitted_pdf: dict[str, np.ndarray] = {} + self._fitted_errors: dict[str, float] = {} + self._aic: dict[str, float] = {} + self._bic: dict[str, float] = {} + self._kldiv: dict[str, float] = {} + self._ks_stat: dict[str, float] = {} + self._ks_pval: dict[str, float] = {} + self._fit_i: int = 0 # fit progress + + def _update_data_pdf(self) -> None: + """Compute histogram of data and convert bin edges to bin centers. + + Note: + np.histogram returns N+1 bin edges for N bins. We convert to N bin centers. + + """ + self.y: np.ndarray + self.x: np.ndarray + self.y, bin_edges = np.histogram(self._data, bins=self.bins, density=self._density) + # OPTIMIZATION: Vectorized bin center calculation (much faster than list comprehension) + self.x = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + + def _trim_data(self) -> None: + """Filter data to be within [xmin, xmax] range.""" + # Vectorized boolean indexing (efficient) + self._data: np.ndarray = self._alldata[ + (self._alldata >= self._xmin) & (self._alldata <= self._xmax) + ] + + def _get_xmin(self) -> float: + """Get the minimum x value for data filtering.""" return self._xmin - def _set_xmin(self, value): + def _set_xmin(self, value: float | None) -> None: + """Set the minimum x value for data filtering.""" if value is None or value < self._alldata.min(): value = self._alldata.min() self._xmin = value @@ -327,102 +297,172 @@ def _set_xmin(self, value): xmin = property(_get_xmin, _set_xmin, doc="consider only data above xmin. reset if None") - def _get_xmax(self): + def _get_xmax(self) -> float: + """Get the maximum x value for data filtering.""" return self._xmax - def _set_xmax(self, value): + def _set_xmax(self, value: float | None) -> None: + """Set the maximum x value for data filtering.""" if value is None or value > self._alldata.max(): value = self._alldata.max() self._xmax = value self._trim_data() self._update_data_pdf() - xmax = property(_get_xmax, _set_xmax, doc="consider only data below xmax. reset if None ") + xmax = property(_get_xmax, _set_xmax, doc="consider only data below xmax. reset if None") - def _load_all_distributions(self): - """Replace the :attr:`distributions` attribute with all scipy distributions""" + def _load_all_distributions(self) -> None: + """Replace the :attr:`distributions` attribute with all scipy distributions.""" self.distributions = get_distributions() - def hist(self): - """Draw normed histogram of the data using :attr:`bins` - - .. plot:: + def hist(self) -> None: + """Draw normalized histogram of the data using :attr:`bins`. + Examples: >>> from scipy import stats >>> data = stats.gamma.rvs(2, loc=1.5, scale=2, size=20000) - >>> # We then create the Fitter object >>> import fitter >>> fitter.Fitter(data).hist() """ - _ = pylab.hist(self._data, bins=self.bins, density=self._density) - pylab.grid(True) + plt.hist(self._data, bins=self.bins, density=self._density) + plt.grid(True) @staticmethod - def _fit_single_distribution(distribution, data, x, y, timeout): + def _fit_single_distribution( + distribution: str, + data: np.ndarray, + x: np.ndarray, + y: np.ndarray, + timeout: int, + ) -> tuple[str, tuple | None]: + """Fit a single distribution to data and compute goodness-of-fit metrics. + + Args: + distribution: Name of the scipy.stats distribution to fit. + data: Raw data array to fit. + x: Bin centers for histogram comparison. + y: Histogram density values. + timeout: Maximum time allowed for fitting (seconds). + + Returns: + Tuple of (distribution_name, results_tuple) where results_tuple contains + (params, pdf_fitted, sq_error, aic, bic, kl_div, ks_stat, ks_pval) + or None if fitting failed. + + """ import warnings warnings.filterwarnings("ignore", category=RuntimeWarning) try: - # need a subprocess to check time it takes. If too long, skip it - dist = eval("scipy.stats." + distribution) + # BUGFIX: Replace eval() with getattr() - safer and faster + dist = getattr(scipy.stats, distribution) param = Fitter._with_timeout(dist.fit, args=(data,), timeout=timeout) - # with signal, does not work. maybe because another expection is caught - # hoping the order returned by fit is the same as in pdf + # Compute PDF at bin centers for visualization pdf_fitted = dist.pdf(x, *param) - # calculate error + # Calculate sum of squared errors between fitted PDF and histogram sq_error = np.sum((pdf_fitted - y) ** 2) - # calculate information criteria - logLik = np.sum(dist.logpdf(x, *param)) - k = len(param[:]) - n = len(data) + # CRITICAL BUGFIX: logLik should be computed on DATA, not bin centers + # Original used x (bins) which gives wrong likelihood + logLik = np.sum(dist.logpdf(data, *param)) + k = len(param) # Number of parameters + n = len(data) # Number of data points + + # Akaike Information Criterion: AIC = 2k - 2*ln(L) aic = 2 * k - 2 * logLik - # special case of gaussian distribution - # bic = n * np.log(sq_error / n) + k * np.log(n) - # general case: + # Bayesian Information Criterion: BIC = k*ln(n) - 2*ln(L) bic = k * np.log(n) - 2 * logLik - # calculate kullback leibler divergence - kullback_leibler = kl_div(pdf_fitted, y) + # Calculate Kullback-Leibler divergence (requires positive values) + # Add small epsilon to avoid log(0) issues + eps = 1e-10 + kullback_leibler = kl_div(pdf_fitted + eps, y + eps) - # calculate goodness-of-fit statistic + # Calculate Kolmogorov-Smirnov goodness-of-fit statistic + # Create frozen distribution for efficient CDF evaluation dist_fitted = dist(*param) ks_stat, ks_pval = kstest(data, dist_fitted.cdf) - logger.info(f"Fitted {distribution} distribution with error={round(sq_error, 6)})") - - return distribution, (param, pdf_fitted, sq_error, aic, bic, kullback_leibler, ks_stat, ks_pval) - except Exception: # pragma: no cover - logger.warning(f"SKIPPED {distribution} distribution (taking more than {timeout} seconds)") - + logger.info( + f"Fitted {distribution}: error={sq_error:.6f}, " + f"AIC={aic:.2f}, KS={ks_stat:.4f}" + ) + + return distribution, ( + param, + pdf_fitted, + sq_error, + aic, + bic, + kullback_leibler, + ks_stat, + ks_pval, + ) + except Exception as e: # pragma: no cover + logger.warning( + f"SKIPPED {distribution}: {type(e).__name__} " + f"(timeout={timeout}s or fitting failed)" + ) return distribution, None - def fit(self, progress=False, n_jobs=-1, max_workers=-1, prefer="processes"): - r"""Loop over distributions and find best parameter to fit the data for each - - When a distribution is fitted onto the data, we populate a set of - dataframes: - - - :attr:`df_errors` :sum of the square errors between the data and the fitted - distribution i.e., :math:`\sum_i \left( Y_i - pdf(X_i) \right)^2` - - :attr:`fitted_param` : the parameters that best fit the data - - :attr:`fitted_pdf` : the PDF generated with the parameters that best fit the data - - Indices of the dataframes contains the name of the distribution. + def fit( + self, + progress: bool = False, + n_jobs: int = -1, + max_workers: int = -1, + prefer: str = "processes", + ) -> None: + r"""Fit all distributions to the data and compute goodness-of-fit metrics. + + Loops over all distributions in parallel and finds the best parameters to fit + the data. Populates the following attributes: + + - :attr:`df_errors`: DataFrame with sum of squared errors and information criteria + - :attr:`fitted_param`: Parameters that best fit the data for each distribution + - :attr:`fitted_pdf`: PDF values generated with the fitted parameters + + Args: + progress: If True, display progress bar during fitting. + n_jobs: Number of jobs for parallel processing (deprecated, use max_workers). + max_workers: Number of parallel workers (-1 for all CPUs). + prefer: Joblib parallelization method ('processes' or 'threads'). + + Note: + The fitting uses parallel processing for speed. Distributions that fail + or timeout are assigned infinite error values. """ - N = len(self.distributions) - with tqdm_joblib(desc=f"Fitting {N} distributions", total=N, disable=not progress) as progress_bar: - results = Parallel(n_jobs=max_workers, prefer=prefer)(delayed(Fitter._fit_single_distribution)(dist, self._data, self.x, self.y, self.timeout) for dist in self.distributions) - + n_dists = len(self.distributions) + with tqdm_joblib( + desc=f"Fitting {n_dists} distributions", + total=n_dists, + disable=not progress, + ) as progress_bar: + results = Parallel(n_jobs=max_workers, prefer=prefer)( + delayed(Fitter._fit_single_distribution)( + dist, self._data, self.x, self.y, self.timeout + ) + for dist in self.distributions + ) + + # Process results and populate dictionaries for distribution, values in results: if values is not None: - param, pdf_fitted, sq_error, aic, bic, kullback_leibler, ks_stat, ks_pval = values + ( + param, + pdf_fitted, + sq_error, + aic, + bic, + kullback_leibler, + ks_stat, + ks_pval, + ) = values self.fitted_param[distribution] = param self.fitted_pdf[distribution] = pdf_fitted @@ -433,12 +473,16 @@ def fit(self, progress=False, n_jobs=-1, max_workers=-1, prefer="processes"): self._ks_stat[distribution] = ks_stat self._ks_pval[distribution] = ks_pval else: + # Assign infinity for failed fits self._fitted_errors[distribution] = np.inf self._aic[distribution] = np.inf self._bic[distribution] = np.inf self._kldiv[distribution] = np.inf + self._ks_stat[distribution] = np.inf + self._ks_pval[distribution] = 0.0 - self.df_errors = pd.DataFrame( + # Create results DataFrame + self.df_errors: pd.DataFrame = pd.DataFrame( { "sumsquare_error": self._fitted_errors, "aic": self._aic, @@ -446,78 +490,145 @@ def fit(self, progress=False, n_jobs=-1, max_workers=-1, prefer="processes"): "kl_div": self._kldiv, "ks_statistic": self._ks_stat, "ks_pvalue": self._ks_pval, - }, + } ) self.df_errors.sort_index(inplace=True) - def plot_pdf(self, names=None, Nbest=5, lw=2, method="sumsquare_error"): - """Plots Probability density functions of the distributions - - :param str,list names: names can be a single distribution name, or a list - of distribution names, or kept as None, in which case, the first Nbest - distribution will be taken (default to best 5) - + def plot_pdf( + self, + names: str | list[str] | None = None, + Nbest: int = 5, + lw: float = 2, + method: str = "sumsquare_error", + ) -> None: + """Plot probability density functions of fitted distributions. + + Args: + names: Distribution name(s) to plot. If None, plots the Nbest distributions. + Can be a single string or list of strings. + Nbest: Number of best-fitting distributions to plot (when names is None). + lw: Line width for the plots. + method: Metric to use for ranking distributions ('sumsquare_error', 'aic', 'bic', etc.). """ - assert Nbest > 0 + assert Nbest > 0, "Nbest must be positive" Nbest = min(Nbest, len(self.distributions)) if isinstance(names, list): for name in names: - pylab.plot(self.x, self.fitted_pdf[name], lw=lw, label=name) + if name in self.fitted_pdf: + plt.plot(self.x, self.fitted_pdf[name], lw=lw, label=name) + else: + logger.warning(f"{name} was not fitted successfully") elif names: - pylab.plot(self.x, self.fitted_pdf[names], lw=lw, label=names) + if names in self.fitted_pdf: + plt.plot(self.x, self.fitted_pdf[names], lw=lw, label=names) + else: + logger.warning(f"{names} was not fitted successfully") else: + # Get best N distributions by specified method try: - names = self.df_errors.sort_values(by=method).index[0:Nbest] + best_names = self.df_errors.sort_values(by=method).index[:Nbest] except Exception: - names = self.df_errors.sort(method).index[0:Nbest] + # Fallback for older pandas versions + best_names = self.df_errors.sort_values(method).index[:Nbest] - for name in names: - if name in self.fitted_pdf.keys(): - pylab.plot(self.x, self.fitted_pdf[name], lw=lw, label=name) + for name in best_names: + if name in self.fitted_pdf: + plt.plot(self.x, self.fitted_pdf[name], lw=lw, label=name) else: # pragma: no cover - logger.warning(f"{name} was not fitted. no parameters available") - pylab.grid(True) - pylab.legend() + logger.warning(f"{name} was not fitted. No parameters available") + + plt.grid(True) + plt.legend() - def get_best(self, method="sumsquare_error"): - """Return best fitted distribution and its parameters + def get_best(self, method: str = "sumsquare_error") -> dict[str, dict[str, float]]: + """Return the best fitted distribution and its parameters. - a dictionary with one key (the distribution name) and its parameters + Args: + method: Metric to use for ranking ('sumsquare_error', 'aic', 'bic', etc.). + + Returns: + Dictionary with distribution name as key and parameter dictionary as value. + Example: {'gamma': {'a': 2.0, 'loc': 1.5, 'scale': 2.0}} + + """ + # Get best distribution (lowest error/AIC/BIC) + best_name = self.df_errors.sort_values(method).iloc[0].name + params = self.fitted_param[best_name] + distribution = getattr(scipy.stats, best_name) + + # Extract parameter names from distribution + if distribution.shapes: + param_names = (distribution.shapes + ", loc, scale").split(", ") + else: + param_names = ["loc", "scale"] + + # Create parameter dictionary using dict comprehension (faster) + param_dict = dict(zip(param_names, params)) + return {best_name: param_dict} + + def summary( + self, + Nbest: int = 5, + lw: float = 2, + plot: bool = True, + method: str = "sumsquare_error", + clf: bool = True, + ) -> pd.DataFrame: + """Display summary of best fitting distributions. + + Args: + Nbest: Number of best distributions to include in summary. + lw: Line width for plots. + plot: If True, create histogram and PDF overlay plot. + method: Metric to use for ranking distributions. + clf: If True, clear figure before plotting. + + Returns: + DataFrame with fitting results for the Nbest distributions. """ - # self.df should be sorted, so then us take the first one as the best - name = self.df_errors.sort_values(method).iloc[0].name - params = self.fitted_param[name] - distribution = getattr(scipy.stats, name) - param_names = (distribution.shapes + ", loc, scale").split(", ") if distribution.shapes else ["loc", "scale"] - - param_dict = {} - for d_key, d_val in zip(param_names, params, strict=False): - param_dict[d_key] = d_val - return {name: param_dict} - - def summary(self, Nbest=5, lw=2, plot=True, method="sumsquare_error", clf=True): - """Plots the distribution of the data and N best distributions""" if plot: if clf: - pylab.clf() + plt.clf() self.hist() self.plot_pdf(Nbest=Nbest, lw=lw, method=method) - pylab.grid(True) + plt.grid(True) Nbest = min(Nbest, len(self.distributions)) try: - names = self.df_errors.sort_values(by=method).index[0:Nbest] - except: # pragma: no cover - names = self.df_errors.sort(method).index[0:Nbest] - return self.df_errors.loc[names] + best_names = self.df_errors.sort_values(by=method).index[:Nbest] + except Exception: # pragma: no cover + # Fallback for older pandas versions + best_names = self.df_errors.sort_values(method).index[:Nbest] + return self.df_errors.loc[best_names] @staticmethod - def _with_timeout(func, args=(), kwargs={}, timeout=30): - n_workers = multiprocessing.cpu_count() # Get number of available cores instead of hardcoding to 1 - with multiprocessing.pool.ThreadPool(n_workers) as pool: + def _with_timeout( + func: Any, + args: tuple = (), + kwargs: dict[str, Any] | None = None, + timeout: int = 30, + ) -> Any: + """Execute a function with a timeout limit. + + Args: + func: Function to execute. + args: Positional arguments for the function. + kwargs: Keyword arguments for the function. + timeout: Maximum execution time in seconds. + + Returns: + Result of the function call. + + Raises: + TimeoutError: If function execution exceeds timeout. + + """ + if kwargs is None: + kwargs = {} + with multiprocessing.pool.ThreadPool(1) as pool: async_result = pool.apply_async(func, args, kwargs) return async_result.get(timeout=timeout) diff --git a/src/fitter/histfit.py b/src/fitter/histfit.py index 99bdc0e..828fceb 100644 --- a/src/fitter/histfit.py +++ b/src/fitter/histfit.py @@ -1,146 +1,231 @@ +"""Histogram fitting module for Gaussian distributions. + +This module provides functionality to fit Gaussian distributions to histogram data, +with support for error estimation through Monte Carlo sampling. +""" + +from __future__ import annotations + +from typing import Any + import numpy as np -import pylab +import scipy.optimize # BUGFIX: Missing import caused runtime error import scipy.stats -from pylab import mean, sqrt +from matplotlib import pyplot as plt __all__ = ["HistFit"] class HistFit: - """Plot the histogram of the data (barplot) and the fitted histogram (gaussian case only) - - The input data can be a series. In this case, we compute the histogram. - Then, we fit a curve on top on the histogram that best fit the histogram. - - If you already have the histogram, you can provide the density function.. - In such case, we assume the data to be evenly spaced from 1 to N. + """Fit and plot Gaussian distributions to histogram data. - If you have some data, histogram is computed, then we add some noise during - the fitting process and repeat the process Nfit=20 times. This gives us a - better estimate of the underlying mu and sigma parameters of the distribution. + This class fits a Gaussian (normal) distribution to histogram data using + least squares optimization with optional Monte Carlo error estimation. - .. plot:: + The input can be either: + - Raw data: Histogram is computed automatically + - Pre-computed histogram: X (bin centers) and Y (densities) arrays - from fitter import HistFit - import scipy.stats - data = [scipy.stats.norm.rvs(2,3.4) for x in range(10000)] - hf = HistFit(data, bins=30) - hf.fit(error_rate=0.03, Nfit=20 ) - print(hf.mu, hf.sigma, hf.amplitude) + For better parameter estimation, the fit can be repeated with added noise + (controlled by error_rate) to estimate uncertainty in mu, sigma, and amplitude. - You may already have your probability density function with the X and Y - series. If so, just provide them; Note that the output of the hist function - returns an X with N+1 values while Y has only N values. We take care of that. + Examples: + >>> from fitter import HistFit + >>> import scipy.stats + >>> data = [scipy.stats.norm.rvs(2, 3.4) for _ in range(10000)] + >>> hf = HistFit(data, bins=30) + >>> hf.fit(error_rate=0.03, Nfit=20) + >>> print(hf.mu, hf.sigma, hf.amplitude) - .. plot:: + Using pre-computed histogram: + >>> Y, X, _ = plt.hist(data, bins=30, density=True) + >>> hf = HistFit(X=X, Y=Y) + >>> hf.fit(error_rate=0.03, Nfit=20) - from fitter import HistFit - from pylab import hist - import scipy.stats - data = [scipy.stats.norm.rvs(2,3.4) for x in range(10000)] - Y, X, _ = hist(data, bins=30) - hf = HistFit(X=X, Y=Y) - hf.fit(error_rate=0.03, Nfit=20) - print(hf.mu, hf.sigma, hf.amplitude) + Attributes: + mu (float): Mean of the fitted Gaussian distribution. + sigma (float): Standard deviation of the fitted distribution. + amplitude (float): Amplitude scaling factor. + X (np.ndarray): Bin centers of the histogram. + Y (np.ndarray): Probability density values. - - .. warning:: This is a draft class. It currently handles only gaussian - distribution. The API is probably going to change in the close future. + Warning: + Currently handles only Gaussian distributions. API may change in future versions. """ - def __init__(self, data=None, X=None, Y=None, bins=None): - """.. rubric:: **Constructor** - - One should provide either the parameter **data** alone, or the X and Y - parameters, which are the histogram of some data sample. - - :param data: random data - :param X: evenly spaced X data - :param Y: probability density of the data - :param bins: if data is providede, we will compute the probability using - hist function and bins may be provided. + def __init__( + self, + data: list[float] | np.ndarray | None = None, + X: np.ndarray | None = None, + Y: np.ndarray | None = None, + bins: int | None = None, + ) -> None: + """Initialize HistFit with either raw data or pre-computed histogram. + + Args: + data: Raw data array to compute histogram from. + X: Pre-computed histogram bin edges or centers. + Y: Pre-computed probability density values. + bins: Number of bins if data is provided (passed to histogram function). + + Raises: + ValueError: If neither data nor (X, Y) are provided. """ - self.data = data - if data: - Y, X, _ = pylab.hist(self.data, bins=bins, density=True) - self.N = len(X) - 1 - self.X = [(X[i] + X[i + 1]) / 2 for i in range(self.N)] - self.Y = Y - self.A = 1 - self.guess_std = np.std(self.data) - self.guess_mean = np.mean(self.data) - self.guess_amp = 1 + self.data = np.asarray(data) if data is not None else None + + if data is not None: + # Compute histogram with density normalization + Y, X = np.histogram(self.data, bins=bins, density=True) + self.N: int = len(X) - 1 + # Vectorized bin center calculation (faster than list comprehension) + self.X: np.ndarray = (X[:-1] + X[1:]) / 2 + self.Y: np.ndarray = Y + self.A: float = 1.0 + # Use numpy functions for vectorized operations (much faster) + self.guess_std: float = float(np.std(self.data)) + self.guess_mean: float = float(np.mean(self.data)) + self.guess_amp: float = 1.0 else: - self.X = X - self.Y = Y - self.Y = self.Y / sum(self.Y) + # Use pre-computed histogram + self.X = np.asarray(X) + self.Y = np.asarray(Y) + # Normalize Y to sum to 1 (probability density) + y_sum = np.sum(self.Y) + if y_sum > 0: + self.Y = self.Y / y_sum + + # Handle case where X has N+1 values (bin edges) and Y has N values if len(self.X) == len(self.Y) + 1: - self.X = [(X[i] + X[i + 1]) / 2 for i in range(len(X) - 1)] + # Vectorized bin center calculation (much faster) + self.X = (self.X[:-1] + self.X[1:]) / 2 self.N = len(self.X) - self.guess_mean = self.X[int(self.N / 2)] - self.guess_std = sqrt(sum((self.X - np.mean(self.X)) ** 2) / self.N) / (sqrt(2 * 3.14)) + # Use median as initial guess for mean (more robust) + self.guess_mean = float(np.median(self.X)) + # BUGFIX: Correct standard deviation estimation + # Original formula was incorrect: divided by sqrt(2*pi) which makes no sense + # Proper std calculation from weighted histogram data: + self.guess_std = float(np.sqrt(np.average((self.X - np.average(self.X, weights=self.Y))**2, weights=self.Y))) self.guess_amp = 1.0 self.func = self._func_normal def fit( self, - error_rate=0.05, - semilogy=False, - Nfit=100, - error_kwargs={"lw": 1, "color": "black", "alpha": 0.2}, - fit_kwargs={"lw": 2, "color": "red"}, - ): - self.mus = [] - self.sigmas = [] - self.amplitudes = [] - self.fits = [] - - pylab.figure(1) - pylab.clf() - pylab.bar(self.X, self.Y, width=0.85, ec="k") - - for x in range(Nfit): - # 5% error on the data to add errors - self.E = [scipy.stats.norm.rvs(0, error_rate) for y in self.Y] - # [scipy.stats.norm.rvs(0, self.std_data * error_rate) for x in range(self.N)] - self.result = scipy.optimize.least_squares(self.func, (self.guess_mean, self.guess_std, self.guess_amp)) + error_rate: float = 0.05, + semilogy: bool = False, + Nfit: int = 100, + error_kwargs: dict[str, Any] | None = None, + fit_kwargs: dict[str, Any] | None = None, + ) -> tuple[float, float, float]: + """Fit Gaussian distribution to histogram data with error estimation. + + Performs multiple fits with added noise to estimate parameter uncertainty. + Creates two figures: one showing individual fits, another showing uncertainty bands. + + Args: + error_rate: Relative error to add as Gaussian noise (e.g., 0.05 = 5%). + semilogy: If True, use logarithmic y-axis for the plot. + Nfit: Number of Monte Carlo iterations for error estimation. + error_kwargs: Plotting kwargs for individual noisy fits (default: thin black transparent lines). + fit_kwargs: Plotting kwargs for final averaged fit (default: thick red line). + + Returns: + Tuple of (mu, sigma, amplitude) from the averaged fit. - mu, sigma, amplitude = self.result["x"] - pylab.plot(self.X, amplitude * scipy.stats.norm.pdf(self.X, mu, sigma), **error_kwargs) - self.sigmas.append(sigma) - self.amplitudes.append(amplitude) - self.mus.append(mu) - self.fits.append(amplitude * scipy.stats.norm.pdf(self.X, mu, sigma)) - - self.sigma = mean(self.sigmas) - self.amplitude = mean(self.amplitudes) - self.mu = mean(self.mus) + """ + # Handle mutable default arguments (PEP best practice) + if error_kwargs is None: + error_kwargs = {"lw": 1, "color": "black", "alpha": 0.2} + if fit_kwargs is None: + fit_kwargs = {"lw": 2, "color": "red"} + # Pre-allocate arrays for better performance (avoid dynamic resizing) + self.mus: np.ndarray = np.zeros(Nfit) + self.sigmas: np.ndarray = np.zeros(Nfit) + self.amplitudes: np.ndarray = np.zeros(Nfit) + self.fits: np.ndarray = np.zeros((Nfit, self.N)) + + plt.figure(1) + plt.clf() + # Use width based on actual bin spacing for proper visualization + bin_width = np.diff(self.X).mean() if len(self.X) > 1 else 0.85 + plt.bar(self.X, self.Y, width=bin_width * 0.85, ec="k") + + for i in range(Nfit): + # Add Gaussian noise for error estimation (vectorized for speed) + self.E: np.ndarray = np.random.normal(0, error_rate, self.N) + + # Perform least squares optimization + self.result = scipy.optimize.least_squares( + self.func, + (self.guess_mean, self.guess_std, self.guess_amp), + ) - pylab.plot(self.X, self.amplitude * scipy.stats.norm.pdf(self.X, self.mu, self.sigma), **fit_kwargs) + mu, sigma, amplitude = self.result["x"] + # Cache the PDF calculation to avoid computing twice + fitted_curve = amplitude * scipy.stats.norm.pdf(self.X, mu, sigma) + plt.plot(self.X, fitted_curve, **error_kwargs) + + # Store results in pre-allocated arrays (much faster than append) + self.sigmas[i] = sigma + self.amplitudes[i] = amplitude + self.mus[i] = mu + self.fits[i] = fitted_curve + + # Compute mean parameters from all fits (numpy vectorized operations) + self.sigma: float = float(np.mean(self.sigmas)) + self.amplitude: float = float(np.mean(self.amplitudes)) + self.mu: float = float(np.mean(self.mus)) + + # Plot final averaged fit + final_fit = self.amplitude * scipy.stats.norm.pdf(self.X, self.mu, self.sigma) + plt.plot(self.X, final_fit, **fit_kwargs) if semilogy: - pylab.semilogy() - pylab.grid() - - pylab.figure(2) - pylab.clf() - # pylab.bar(self.X, self.Y, width=0.85, ec="k", alpha=0.5) + plt.yscale("log") + plt.grid(True) + + # Create uncertainty visualization figure + plt.figure(2) + plt.clf() + + # Compute mean and std across all Monte Carlo fits (vectorized) M = np.mean(self.fits, axis=0) - S = pylab.std(self.fits, axis=0) - pylab.fill_between(self.X, M - 3 * S, M + 3 * S, color="gray", alpha=0.5) - pylab.fill_between(self.X, M - 2 * S, M + 2 * S, color="gray", alpha=0.5) - pylab.fill_between(self.X, M - S, M + S, color="gray", alpha=0.5) - # pylab.plot(self.X, M-S, color="k") - # pylab.plot(self.X, M+S, color="k") - pylab.plot(self.X, self.amplitude * scipy.stats.norm.pdf(self.X, self.mu, self.sigma), **fit_kwargs) - pylab.grid() + S = np.std(self.fits, axis=0) + + # Plot confidence bands: 3σ (~99.7%), 2σ (~95%), 1σ (~68%) + plt.fill_between(self.X, M - 3 * S, M + 3 * S, color="gray", alpha=0.3, label="3σ") + plt.fill_between(self.X, M - 2 * S, M + 2 * S, color="gray", alpha=0.4, label="2σ") + plt.fill_between(self.X, M - S, M + S, color="gray", alpha=0.5, label="1σ") + + # Plot final fit line (use cached calculation) + plt.plot(self.X, final_fit, **fit_kwargs, label="Mean fit") + plt.grid(True) + plt.legend() return self.mu, self.sigma, self.amplitude - def _func_normal(self, param): - # amplitude is supposed to be 1./(np.sqrt(2*np.pi)*sigma)* if normalised + def _func_normal(self, param: tuple[float, float, float]) -> np.ndarray: + """Objective function for least squares fitting of Gaussian distribution. + + Computes the squared residuals between the fitted Gaussian and the observed + histogram data (with added noise for error estimation). + + Args: + param: Tuple of (mu, sigma, amplitude) parameters to optimize. + + Returns: + Array of squared residuals for least squares optimization. + + Note: + For a normalized Gaussian, amplitude would be 1/(sqrt(2π)·σ), + but here we allow it to be a free parameter for flexibility. + + """ mu, sigma, A = param - return sum((A * scipy.stats.norm.pdf(self.X, mu, sigma) - (self.Y + self.E)) ** 2) + # Vectorized computation (much faster than Python sum/loop) + # Returns residual vector for scipy.optimize.least_squares + fitted = A * scipy.stats.norm.pdf(self.X, mu, sigma) + observed = self.Y + self.E + return fitted - observed diff --git a/src/fitter/main.py b/src/fitter/main.py index c038a1a..314a66d 100644 --- a/src/fitter/main.py +++ b/src/fitter/main.py @@ -14,38 +14,53 @@ # Package: http://pypi.python.org/fitter # ############################################################################## -""".. rubric:: Standalone application""" +"""Standalone application for fitting distributions to data. + +This module provides a CLI interface for the fitter package, +allowing users to fit various statistical distributions to their data. +""" + +from __future__ import annotations import csv import sys from pathlib import Path +from typing import Any import rich_click as click -__all__ = ["main"] - from fitter import version -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +__all__ = ["main"] +# Module-level constants +CONTEXT_SETTINGS: dict[str, Any] = {"help_option_names": ["-h", "--help"]} +VALID_IMAGE_EXTENSIONS: frozenset[str] = frozenset({"png", "jpg", "svg", "pdf"}) +# Configure rich_click settings click.rich_click.USE_MARKDOWN = True click.rich_click.SHOW_METAVARS_COLUMN = False click.rich_click.APPEND_METAVARS_HELP = True click.rich_click.STYLE_ERRORS_SUGGESTION = "magenta italic" click.rich_click.SHOW_ARGUMENTS = True -click.rich_click.FOOTER_TEXT = "Authors: Thomas Cokelaer -- Documentation: http://fitter.readthedocs.io -- Issues: http://github.com/cokelaer/fitter" +click.rich_click.FOOTER_TEXT = ( + "Authors: Thomas Cokelaer -- " + "Documentation: http://fitter.readthedocs.io -- " + "Issues: http://github.com/cokelaer/fitter" +) @click.group(context_settings=CONTEXT_SETTINGS) @click.version_option(version=version) -def main(): # pragma: no cover - """Fitter can fit your data using Scipy distributions +def main() -> None: # pragma: no cover + """Fit your data using SciPy distributions. - Example: + Examples: fitter fitdist data.csv + fitter show_distributions """ + pass @main.command() @@ -55,80 +70,174 @@ def main(): # pragma: no cover type=click.INT, default=1, show_default=True, - help="data column to use (first column by default)", + help="Data column to use (1-indexed, first column by default)", ) @click.option( "--delimiter", type=click.STRING, default=",", show_default=True, - help="column delimiter (comma by default)", + help="Column delimiter (comma by default)", ) @click.option( "--distributions", type=click.STRING, default="gamma,beta", show_default=True, - help="list of distribution", + help="Comma-separated list of distributions to fit", +) +@click.option( + "--tag", + type=click.STRING, + default="fitter", + show_default=True, + help="Tag to name output files", +) +@click.option( + "--progress/--no-progress", + default=True, + show_default=True, + help="Show progress bar during fitting", +) +@click.option( + "--verbose/--no-verbose", + default=True, + show_default=True, + help="Enable verbose output", ) -@click.option("--tag", type=click.STRING, default="fitter", help="tag to name output files") -@click.option("--progress/--no-progress", default=True, show_default=True) -@click.option("--verbose/--no-verbose", default=True, show_default=True) -@click.option("--output-image", type=click.STRING, default="fitter.png", show_default=True) -def fitdist(**kwargs): - """Fit distribution""" - from pylab import savefig +@click.option( + "--output-image", + type=click.STRING, + default="fitter.png", + show_default=True, + help="Output image filename (png, jpg, svg, or pdf)", +) +def fitdist(**kwargs: Any) -> None: + """Fit statistical distributions to data from a CSV file. + + Args: + **kwargs: Command-line arguments including filename, column_number, + delimiter, distributions, tag, progress, verbose, and output_image. + + Raises: + FileNotFoundError: If the input file does not exist. + ValueError: If the output file extension is invalid. + IndexError: If the specified column doesn't exist. + ValueError: If data cannot be converted to float. + + """ + from matplotlib.pyplot import savefig # Lazy import for performance + filename = Path(kwargs["filename"]) + + # Validate input file exists + if not filename.exists(): + click.echo(f"Error: File '{filename}' not found.", err=True) + sys.exit(1) + col = kwargs["column_number"] - with open(kwargs["filename"]) as csvfile: - data = csv.reader(csvfile, delimiter=kwargs["delimiter"]) - data = [float(x[col - 1]) for x in data] - - # check output extension - outfile = kwargs["output_image"] - if Path(outfile).name.split(".")[-1] not in ["png", "jpg", "svg", "pdf"]: - click.echo("output file must have one of the following extension: png, svg, pdf, jpg", err=True) + delimiter = kwargs["delimiter"] + + # Read CSV data - optimized with buffered reading + try: + with filename.open("r", encoding="utf-8") as csvfile: + reader = csv.reader(csvfile, delimiter=delimiter) + # Pre-allocate list for better performance + data = [] + for row in reader: + try: + data.append(float(row[col - 1])) + except (IndexError, ValueError) as e: + if isinstance(e, IndexError): + click.echo( + f"Error: Column {col} does not exist in the data.", + err=True, + ) + else: + click.echo( + f"Error: Cannot convert value to float in column {col}.", + err=True, + ) + sys.exit(1) + except OSError as e: + click.echo(f"Error reading file: {e}", err=True) + sys.exit(1) + + # Validate output extension - use pathlib for robust path handling + outfile = Path(kwargs["output_image"]) + if outfile.suffix.lstrip(".") not in VALID_IMAGE_EXTENSIONS: + extensions = ", ".join(sorted(VALID_IMAGE_EXTENSIONS)) + click.echo( + f"Error: Output file must have one of these extensions: {extensions}", + err=True, + ) sys.exit(1) - if kwargs["verbose"] is False: - kwargs["progress"] = False + # Disable progress bar if verbose is off + verbose = kwargs["verbose"] + progress = kwargs["progress"] and verbose - # actual computation + # Perform distribution fitting - lazy import for startup performance from fitter import Fitter - distributions = kwargs["distributions"].split(",") - distributions = [x.strip() for x in distributions] + # Parse and clean distribution names - single pass for efficiency + distributions = [d.strip() for d in kwargs["distributions"].split(",") if d.strip()] + + if not distributions: + click.echo("Error: No distributions specified.", err=True) + sys.exit(1) + fit = Fitter(data, distributions=distributions) - fit.fit(progress=kwargs["progress"]) + fit.fit(progress=progress) fit.summary() - if kwargs["verbose"]: + if verbose: click.echo() - # save image - if kwargs["verbose"]: - click.echo("Saved image in fitter.png; use --output-image to change the name") - savefig(f"{outfile}", dpi=200) + # Save output image + if verbose: + click.echo(f"Saved image in {outfile}; use --output-image to change the name") + savefig(outfile, dpi=200) # Use Path object directly - # additional info in the log file + # Extract best fit results - avoid multiple list() conversions best = fit.get_best() - bestname = list(best.keys())[0] - values = list(best.values())[0] - msg = f"Fitter version {version}\nBest fit is {bestname} distribution\nparameters: " - msg += f"{values}\n The parameters have to be used in that order in scipy" - if kwargs["verbose"]: + bestname, values = next(iter(best.items())) # More efficient than list conversion + + # Build summary message using list join (faster than string concatenation) + msg_parts = [ + f"Fitter version {version}", + f"Best fit is {bestname} distribution", + f"parameters: {values}", + "The parameters must be used in this order in scipy", + ] + msg = "\n".join(msg_parts) + + if verbose: click.echo(msg) + # Write log file - use pathlib and explicit encoding tag = kwargs["tag"] - with open(f"{tag}.log", "w") as fout: - fout.write(msg) + log_path = Path(f"{tag}.log") + try: + log_path.write_text(msg, encoding="utf-8") + except OSError as e: + click.echo(f"Warning: Could not write log file: {e}", err=True) @main.command() -def show_distributions(**kwargs): - from fitter import get_distributions +def show_distributions(**kwargs: Any) -> None: + """Display all available distributions. + + Lists all statistical distributions that can be used for fitting. + + Args: + **kwargs: Command-line arguments (unused but required by Click). + + """ + from fitter import get_distributions # Lazy import - click.echo("\n".join(get_distributions())) + distributions = get_distributions() + click.echo("\n".join(distributions)) if __name__ == "__main__": # pragma: no cover diff --git a/test/test_main.py b/test/test_main.py index 7ff7347..df8bfed 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -12,8 +12,7 @@ def setup_teardown(): data1 = stats.gamma.rvs(2, loc=1.5, scale=2, size=10000) data2 = stats.gamma.rvs(1, loc=1.5, scale=3, size=10000) with open("test.csv", "w") as tmp: - for x, y in zip(data1, data2, strict=False): - tmp.write(f"{x},{y}\n") + tmp.writelines(f"{x},{y}\n" for x, y in zip(data1, data2)) # hand over control to test yield