diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3ebac7c --- /dev/null +++ b/.clang-format @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# clang-format configuration file. Intended for clang-format >= 11. +# +# For more information, see: +# +# Documentation/process/clang-format.rst +# https://clang.llvm.org/docs/ClangFormat.html +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false + +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentGotoLabels: false +IndentPPDirectives: None +IndentWidth: 8 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 8 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true + +# Taken from git's rules +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 10 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 60 + +PointerAlignment: Right +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatementsExceptForEachMacros +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp03 +TabWidth: 8 +UseTab: Always +... diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..19f8158 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: lithammer diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3b5ca19 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..75e25e8 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,80 @@ +name: Python + +on: + push: + branches: + - master + pull_request: + branches: + - master + release: + types: + - published + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + strategy: + matrix: + runs-on: [ubuntu-latest, windows-latest, macos-14] + + steps: + - uses: actions/checkout@v4 + + # Needed to build non-native architectures. + # https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation + - name: Setup QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v2.23.3 + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.runs-on }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: dist/*.tar.gz + + publish: + needs: + - build_wheels + - build_sdist + environment: + name: pypi + url: https://pypi.org/p/jump-consistent-hash + permissions: + id-token: write + runs-on: ubuntu-latest + # if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v4 + with: + pattern: cibw-* + path: dist + merge-multiple: true + + # - name: Publish packages to TestPyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository_url: https://test.pypi.org/legacy/ + + - name: Publish packages to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 71a6b76..050d3ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ -.cache -.tox +compile_commands.json *.py[co] -*.egg-info *.so -build -docs/_build + +*.egg-info/ +.cache/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.venv/ + +__pycache__/ +build/ +dist/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 412d720..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python - -python: - - "2.6" - - "2.7" - - "3.4" - - "3.5" - - "3.6" - -install: - - "pip install 'flake8<3' pytest" - -script: - - make lint - - make test - -sudo: false diff --git a/LICENSE b/LICENSE index 9cc7533..dee3d1d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Peter Renström +Copyright (c) 2018 Peter Lithammer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index daf212d..1aba38f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1 @@ include LICENSE -recursive-include jump *.h diff --git a/Makefile b/Makefile index 05c4768..7048a6f 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,28 @@ -PYTHON ?= python -TESTRUNNER ?= py.test -TESTRUNNERFLAGS ?= -v tests -LINT := flake8 -LINTFLAGS := jump +VIRTUALENV = .venv +UV_RUN = uv run -- all: build -build: build_ext +compile_commands.json: build + bear -- $(UV_RUN) python setup.py build_ext -qf -build_ext: _jump.so +build: $(VIRTUALENV)/uv.lock -_jump.so: jump/jump.cpp jump/jump.h jump/jumpmodule.c - $(PYTHON) setup.py build_ext --inplace +$(VIRTUALENV)/uv.lock: uv.lock pyproject.toml + uv sync + @cp $< $@ -test: build_ext - $(TESTRUNNER) $(TESTRUNNERFLAGS) - -test-all: - tox --skip-missing-interpreters +.PHONY: test +test: build + $(UV_RUN) pytest -v .PHONY: lint -lint: - $(LINT) $(LINTFLAGS) +lint: build + $(UV_RUN) ruff check --diff $(CURDIR) + $(UV_RUN) ruff format --check --diff $(CURDIR) + $(UV_RUN) mypy $(CURDIR) + clang-format --dry-run --Werror --style=file src/jump/*.c .PHONY: clean clean: - $(RM) _jump*.so - $(RM) -r build dist *.egg-info docs/_build .tox - find . -name "*.py[co]" -delete - find . -name __pycache__ | xargs rm -rf - -.PHONY: docs -docs: build_ext - $(MAKE) -C $@ html + git clean -Xdf diff --git a/README.rst b/README.rst index 6274b91..110423a 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,12 @@ Jump Consistent Hash -------------------- -.. image:: https://travis-ci.org/renstrom/python-jump-consistent-hash.svg?branch=master +.. image:: https://github.com/lithammer/python-jump-consistent-hash/workflows/Python/badge.svg :alt: Build Status - :target: https://travis-ci.org/renstrom/python-jump-consistent-hash + :target: https://github.com/lithammer/python-jump-consistent-hash/actions -Python implementation of the jump consistent hash algorithm by John Lamping and -Eric Veach[1]. Tested on Python 2.6, 2.7 and 3.4+. +Python and C implementation of the jump consistent hash algorithm by John +Lamping and Eric Veach[1]. Tested on Python 3.8+. Install ------- @@ -16,10 +16,8 @@ terminal of choice:: $ pip install jump-consistent-hash -Unless running PyPy the installation will try to compile the C++ reference -implementation (unless an appropriate wheel is available). If it fails it will -fallback to the pure Python implementation which is about 10x slower on -CPython. +The C implementation is optional but is about 10x faster than the pure Python +implementation in CPython. Usage ````` @@ -37,17 +35,17 @@ using Python 3: .. code:: python >>> import hashlib - >>> int(hashlib.md5(b'127.0.0.1').hexdigest(), 16) + >>> int(hashlib.md5(b"127.0.0.1").hexdigest(), 16) 325870950296970981340734819828239218902 >>> int(hashlib.sha1(b"127.0.0.1").hexdigest(), 16) 431133456357828263809343936597625557575256328153 >>> import binascii - >>> binascii.crc32(b'127.0.0.1') & 0xffffffff + >>> binascii.crc32(b"127.0.0.1") & 0xffffffff 3619153832 - >>> abs(hash('127.0.0.1')) + >>> abs(hash("127.0.0.1")) 7536019783825143230 Links diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 7afe85b..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,230 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) - $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/JumpConsistentHash.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/JumpConsistentHash.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/JumpConsistentHash" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/JumpConsistentHash" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 4e7fd3a..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Jump Consistent Hash documentation build configuration file, created by -# sphinx-quickstart on Wed May 4 10:07:29 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import os -import sys - -if os.environ.get('READTHEDOCS'): - try: - from unittest.mock import MagicMock - except ImportError: - from mock import MagicMock - - class Mock(MagicMock): - - @classmethod - def __getattr__(cls, name): - return Mock() - - MOCK_MODULES = ['_jump'] - sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon' -] - -# Add any paths that contain templates here, relative to this directory. -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' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Jump Consistent Hash' -copyright = u'2016, Peter Renström' -author = u'Peter Renström' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'2.0.3' -# The full version, including alpha/beta/rc tags. -release = u'2.0.3' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -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 -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -#html_title = u'Jump Consistent Hash v2.0.3' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# 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'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'JumpConsistentHashdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# 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', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'JumpConsistentHash.tex', u'Jump Consistent Hash Documentation', - u'Peter Renström', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'jumpconsistenthash', u'Jump Consistent Hash Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'JumpConsistentHash', u'Jump Consistent Hash Documentation', - author, 'JumpConsistentHash', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 59088d0..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,63 +0,0 @@ -Jump Consistent Hash documentation! -================================================ - -.. toctree:: - :maxdepth: 2 - -Python implementation of the jump consistent hash algorithm by John Lamping and -Eric Veach [1]_. Tested on Python 2.6, 2.7 and 3.4+. - -.. [1] http://arxiv.org/pdf/1406.2294v1.pdf - -Install -------- - -To install Jump Consistent Hash, simply run this simple command in your terminal of choice:: - - $ pip install jump-consistent-hash - -Unless running PyPy the installation will try to compile the C++ reference -implementation (unless an appropriate wheel is available). If it fails it will -fallback to the pure Python implementation if it fails which is about 10x -slower on CPython. - -Usage ------ - -.. code:: python - - >>> import jump - >>> jump.hash(256, 1024) - 520 - -If you want to use a ``str`` as a key instead of an ``int``, you can pass it -through a hash function to compute a real key. Here's a couple of examples -using Python 3: - -.. code:: python - - >>> import hashlib - >>> int(hashlib.md5(b'127.0.0.1').hexdigest(), 16) - 325870950296970981340734819828239218902 - - >>> int(hashlib.sha1(b"127.0.0.1").hexdigest(), 16) - 431133456357828263809343936597625557575256328153 - - >>> import binascii - >>> binascii.crc32(b'127.0.0.1') & 0xffffffff - 3619153832 - - >>> abs(hash('127.0.0.1')) - 7536019783825143230 - -API Reference -------------- - -.. function:: jump.hash(key, num_buckets) - - Generate a number in the range [0, num_buckets). - - :param int key: The key to hash. - :param int num_buckets: Number of buckets to use. - :returns: The bucket number `key` computes to. - :raises ValueError: If `num_buckets` is not a positive number. diff --git a/jump/jump.cpp b/jump/jump.cpp deleted file mode 100644 index 2a18230..0000000 --- a/jump/jump.cpp +++ /dev/null @@ -1,15 +0,0 @@ -extern "C" { -#include "jump.h" -} - -int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) { - int64_t b = -1, j = 0; - - while (j < num_buckets) { - b = j; - key = key * 2862933555777941757ULL + 1; - j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1)); - } - - return (int32_t)b; -} diff --git a/jump/jump.h b/jump/jump.h deleted file mode 100644 index b50eb4f..0000000 --- a/jump/jump.h +++ /dev/null @@ -1,3 +0,0 @@ -#include - -int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets); diff --git a/jump/jumpmodule.c b/jump/jumpmodule.c deleted file mode 100644 index f108fc2..0000000 --- a/jump/jumpmodule.c +++ /dev/null @@ -1,61 +0,0 @@ -#include - -#include "jump.h" - -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#endif - -PyDoc_STRVAR(hash__doc__, "hash(key, num_buckets) -> int\n\ -\n\ -Generate a number in the range [0, num_buckets).\n\ -\n\ -This function uses C bindings for speed.\n\ -\n\ -Args:\n\ - key (int): The key to hash.\n\ - num_buckets (int): Number of buckets to use.\n\ -\n\ -Returns:\n\ - The bucket number `key` computes to.\n\ -\n\ -Raises:\n\ - ValueError: If `num_buckets` is not a positive number.\n"); - -PyDoc_STRVAR(jump__doc__, "Fast, minimal memory, consistent hash algorithm."); - -static PyObject *jump_hash(PyObject *self, PyObject *args) { - uint64_t key; - int32_t num_buckets, h; - - if (!PyArg_ParseTuple(args, "Ki", &key, &num_buckets)) { - return NULL; - } - - if (num_buckets < 1) { - PyErr_SetString(PyExc_ValueError, "num_buckets must be a positive number"); - return NULL; - } - - h = JumpConsistentHash(key, num_buckets); - return Py_BuildValue("i", h); -} - -static PyMethodDef JumpMethods[] = { - {"hash", jump_hash, METH_VARARGS, hash__doc__}, {NULL, NULL, 0, NULL}}; - -#ifdef IS_PY3K -static struct PyModuleDef jumpmodule = {PyModuleDef_HEAD_INIT, "jump", - jump__doc__, -1, JumpMethods}; - -PyMODINIT_FUNC PyInit__jump(void) -#else -void init_jump(void) -#endif -{ -#ifdef IS_PY3K - return PyModule_Create(&jumpmodule); -#else - (void)Py_InitModule3("_jump", JumpMethods, jump__doc__); -#endif -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..275b821 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[build-system] +requires = ["setuptools>=61", "setuptools-scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "jump-consistent-hash" +description = "Implementation of the Jump Consistent Hash algorithm" +readme = "README.rst" +license = { text = "MIT" } +requires-python = ">=3.8" + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "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", + "Programming Language :: Python :: 3.13", +] +keywords = ["jump", "consistent", "hash", "jumphash", "algorithm"] + +dynamic = ["version"] + +[[project.authors]] +name = "Peter Lithammer" +email = "peter.lithammer@gmail.com" + +[project.urls] +Source = "https://github.com/lithammer/python-jump-consistent-hash" +Issues = "https://github.com/lithammer/python-jump-consistent-hash/issues" +Changelog = "https://github.com/lithammer/python-jump-consistent-hash/releases" +Documentation = "https://github.com/lithammer/python-jump-consistent-hash#readme" + +[tool.setuptools_scm] + +[tool.cibuildwheel] +test-command = "pytest {package}/tests" +test-requires = "pytest" + +[tool.cibuildwheel.linux] +archs = ["auto", "aarch64"] + +[tool.uv] +dev-dependencies = [ + "mypy", + "ruff", + "pytest", + "setuptools", # Required for `python setup.py build_ext` +] + +[tool.mypy] +check_untyped_defs = true +ignore_missing_imports = true +no_implicit_optional = true +pretty = true +show_column_numbers = true +warn_unused_configs = true + +[tool.ruff] +line-length = 79 + +[tool.ruff.lint] +select = ["E", "F", "I"] +ignore = ["E501"] + +[tool.ruff.lint.isort] +force-sort-within-sections = true +order-by-type = true diff --git a/setup.py b/setup.py index 4fce878..56e83ab 100644 --- a/setup.py +++ b/setup.py @@ -1,98 +1,13 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function +import os -import platform -import sys +from setuptools import Extension, setup -from distutils.command.build_ext import build_ext -from distutils.errors import (CCompilerError, DistutilsExecError, - DistutilsPlatformError) -from setuptools import setup, find_packages, Extension - -with open('README.rst') as f: - __doc__ = f.read() - -IS_PYPY = platform.python_implementation().lower() == 'pypy' - -if sys.platform == 'win32' and sys.version_info > (2, 6): - # 2.6's distutils.msvc9compiler can raise an IOError when failing to - # find the compiler. - # It can also raise ValueError http://bugs.python.org/issue7511 - ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError, - IOError, ValueError) -else: - ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError) - - -class BuildFailedError(Exception): - pass - - -# This class allows C extension building to fail. -class ve_build_ext(build_ext): - - def run(self): - try: - build_ext.run(self) - except DistutilsPlatformError: - raise BuildFailedError() - - def build_extension(self, ext): - try: - build_ext.build_extension(self, ext) - except ext_errors: - raise BuildFailedError() - - -ext_modules = { - 'ext_modules': [ - Extension('_jump', sources=[ - 'jump/jump.cpp', - 'jump/jumpmodule.c' - ]) +setup( + ext_modules=[ + Extension( + "_jump", + sources=["src/jump/jump.c"], + optional=os.environ.get("CIBUILDWHEEL", "0") != "1", + ) ] -} - - -def run_setup(with_binary): - kwargs = ext_modules if with_binary else {} - - setup(name='jump_consistent_hash', - version='3.0.1', - description='Implementation of the Jump Consistent Hash algorithm', - long_description=__doc__, - author='Peter Renström', - license='MIT', - url='https://github.com/renstrom/python-jump-consistent-hash', - packages=find_packages(), - cmdclass={'build_ext': ve_build_ext}, - keywords=[ - 'jump hash', - 'jumphash', - 'jump consistent hash', - 'consistent hash', - 'hash algorithm', - 'hash' - ], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - ], - **kwargs) - - -try: - run_setup(not IS_PYPY) -except BuildFailedError: - run_setup(False) - print('*' * 75) - print('WARNING: The C extension could not be compiled, ' - 'speedups are not enabled.') - print('Plain-Python installation succeeded.') - print('*' * 75) +) diff --git a/jump/__init__.py b/src/jump/__init__.py similarity index 70% rename from jump/__init__.py rename to src/jump/__init__.py index a273c66..1d2c3e7 100644 --- a/jump/__init__.py +++ b/src/jump/__init__.py @@ -1,19 +1,11 @@ - """Fast, minimal memory, consistent hash algorithm.""" -from __future__ import absolute_import - -import sys - try: from _jump import hash as c_hash except ImportError: c_hash = None -__all__ = ['hash'] - -if sys.version_info[0] > 2: - long = int +__all__ = ["hash"] def py_hash(key, num_buckets): @@ -29,14 +21,16 @@ def py_hash(key, num_buckets): Raises: ValueError: If `num_buckets` is not a positive number. """ - b, j = -1, 0 + b, j = -1, 0.0 if num_buckets < 1: - raise ValueError('num_buckets must be a positive number') + raise ValueError( + f"'num_buckets' must be a positive number, got {num_buckets}" + ) while j < num_buckets: b = int(j) - key = ((key * long(2862933555777941757)) + 1) & 0xffffffffffffffff + key = ((key * int(2862933555777941757)) + 1) & 0xFFFFFFFFFFFFFFFF j = float(b + 1) * (float(1 << 31) / float((key >> 33) + 1)) return int(b) diff --git a/src/jump/__init__.pyi b/src/jump/__init__.pyi new file mode 100644 index 0000000..fa57a74 --- /dev/null +++ b/src/jump/__init__.pyi @@ -0,0 +1,3 @@ +def hash(key: int, num_buckets: int) -> int: ... +def py_hash(key: int, num_buckets: int) -> int: ... +def c_hash(key: int, num_buckets: int) -> int: ... diff --git a/src/jump/jump.c b/src/jump/jump.c new file mode 100644 index 0000000..5d2d08a --- /dev/null +++ b/src/jump/jump.c @@ -0,0 +1,65 @@ +#include +#include + +#include + +static int32_t jump_consistent_hash(uint64_t key, int32_t num_buckets) +{ + int64_t b = -1, j = 0; + + while (j < num_buckets) { + b = j; + key = key * 2862933555777941757ULL + 1; + j = (b + 1) * ((double)(1LL << 31) / (double)((key >> 33) + 1)); + } + + return (int32_t)b; +} + +PyDoc_STRVAR(hash__doc__, "hash(key, num_buckets) -> int\n\ +\n\ +Generate a number in the range [0, num_buckets).\n\ +\n\ +This function uses C bindings for speed.\n\ +\n\ +Args:\n\ + key (int): The key to hash.\n\ + num_buckets (int): Number of buckets to use.\n\ +\n\ +Returns:\n\ + The bucket number `key` computes to.\n\ +\n\ +Raises:\n\ + ValueError: If `num_buckets` is not a positive number.\n"); + +PyDoc_STRVAR(jump__doc__, "Fast, minimal memory, consistent hash algorithm."); + +static PyObject *jump_hash(PyObject *self, PyObject *args) +{ + uint64_t key; + int32_t num_buckets; + + if (!PyArg_ParseTuple(args, "Ki", &key, &num_buckets)) + return NULL; + + if (num_buckets < 1) { + PyErr_Format(PyExc_ValueError, + "'num_buckets' must be a positive number, got %d", + num_buckets); + return NULL; + } + + return Py_BuildValue("i", jump_consistent_hash(key, num_buckets)); +} + +static PyMethodDef jump_methods[] = { { "hash", jump_hash, METH_VARARGS, + hash__doc__ }, + { NULL, NULL, 0, NULL } }; + +static struct PyModuleDef jumpmodule = { PyModuleDef_HEAD_INIT, "jump", + jump__doc__, -1, jump_methods }; + +PyMODINIT_FUNC PyInit__jump(void) +{ + return PyModule_Create(&jumpmodule); +} diff --git a/tests/__init__.py b/src/jump/py.typed similarity index 100% rename from tests/__init__.py rename to src/jump/py.typed diff --git a/tests/test_jump.py b/tests/test_jump.py index b6024b1..ee99cfb 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -5,6 +5,7 @@ class JumpConsistentHashTestCase(TestCase): + """Test the pure Python version.""" def test_hash(self): h = jump.py_hash(1, 1) @@ -27,10 +28,11 @@ def test_negative_bucket_number(self): self.assertRaises(ValueError, jump.py_hash, 0xDEAD10CC, -666) def test_very_large_key(self): - jump.py_hash(int(hashlib.sha1(b'abc').hexdigest(), 16), 5) + jump.py_hash(int(hashlib.sha1(b"abc").hexdigest(), 16), 5) class JumpConsistentHashExtensionTestCase(TestCase): + """Test the C extension version.""" def test_hash(self): h = jump.c_hash(1, 1) @@ -53,4 +55,4 @@ def test_negative_bucket_number(self): self.assertRaises(ValueError, jump.c_hash, 0xDEAD10CC, -666) def test_very_large_key(self): - jump.c_hash(int(hashlib.sha1(b'abc').hexdigest(), 16), 5) + jump.c_hash(int(hashlib.sha1(b"abc").hexdigest(), 16), 5) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index befd2c7..0000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tox] -envlist = py26,py27,py34,py35,py36 - -[testenv] -deps = pytest -commands = py.test diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..64d566e --- /dev/null +++ b/uv.lock @@ -0,0 +1,187 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jump-consistent-hash" +version = "3.5.1.dev2+gb278b2f.d20240821" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "setuptools" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "setuptools" }, +] + +[[package]] +name = "mypy" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/ba/858cc9631c24a349c1c63814edc16448da7d6b8716b2c83a10aa20f5ee89/mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c", size = 10937885 }, + { url = "https://files.pythonhosted.org/packages/2d/88/2ae81f7489da8313d0f2043dd657ba847650b00a0fb8e07f40e716ed8c58/mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411", size = 10111978 }, + { url = "https://files.pythonhosted.org/packages/df/4b/d211d6036366f9ea5ee9fb949e80d133b4b8496cdde78c7119f518c49734/mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03", size = 12498441 }, + { url = "https://files.pythonhosted.org/packages/94/d2/973278d03ad11e006d71d4c858bfe45cf571ae061f3997911925c70a59f0/mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4", size = 13020595 }, + { url = "https://files.pythonhosted.org/packages/0b/c2/7f4285eda528883c5c34cb4b8d88080792967f7f7f24256ad8090d303702/mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58", size = 9568307 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/62d8ce619493a5364dda4f410912aa12c27126926e8fb8393edca0664640/mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5", size = 10858723 }, + { url = "https://files.pythonhosted.org/packages/fe/aa/2ad15a318bc6a17b7f23e1641a624603949904f6131e09681f40340fb875/mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca", size = 10038078 }, + { url = "https://files.pythonhosted.org/packages/4d/7f/77feb389d91603f55b3c4e3e16ccf8752bce007ed73ca921e42c9a5dff12/mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de", size = 12420213 }, + { url = "https://files.pythonhosted.org/packages/bc/5b/907b4681f68e7ee2e2e88eed65c514cf6406b8f2f83b243ea79bd4eddb97/mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809", size = 12898278 }, + { url = "https://files.pythonhosted.org/packages/5b/b3/2a83be637825d7432b8e6a51e45d02de4f463b6c7ec7164a45009a7cf477/mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72", size = 9564438 }, + { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789 }, + { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696 }, + { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772 }, + { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190 }, + { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138 }, + { url = "https://files.pythonhosted.org/packages/f7/8c/805ee92b0a94b61593787e2863d8551383fe1d344238433088e509accad3/mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2", size = 10872601 }, + { url = "https://files.pythonhosted.org/packages/f2/5d/9d705fa6240c494223d231a7e37904c9da972e44a5bbbfda7cac169fe925/mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b", size = 10065192 }, + { url = "https://files.pythonhosted.org/packages/dd/0a/921c07e284cf063beb4b4b9abf96354dec52ea692a13061bada479dfbeeb/mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0", size = 12459539 }, + { url = "https://files.pythonhosted.org/packages/e4/b4/4c54cbae97d50e53137560508f144f089d3bc2bd204a5f4223ce698e5cb3/mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd", size = 12961770 }, + { url = "https://files.pythonhosted.org/packages/5a/3a/859a3c6454d1b20b5a055e223a516fe825e15e98639e39d10d00b7e55dee/mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb", size = 9549484 }, + { url = "https://files.pythonhosted.org/packages/d9/3d/cca659545bd83c67ab784ac2221f8264b3b74e98b2a5aee4f454276ecaf2/mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe", size = 10936143 }, + { url = "https://files.pythonhosted.org/packages/83/ff/ec6f1015813adb183925b6f9696062b7fb60b1d60761cc06cea85dd7bcb0/mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c", size = 10105432 }, + { url = "https://files.pythonhosted.org/packages/08/a3/ffc2ee9b8689920a524a8013bf05482f51d191d73a8a3939f3d47d9a485a/mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69", size = 12493869 }, + { url = "https://files.pythonhosted.org/packages/7b/6b/830fbf3066dcf79831f360c5b1ae3923cfc248aa6a32ea217f179a210126/mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74", size = 13016443 }, + { url = "https://files.pythonhosted.org/packages/0a/ae/8ebca638f6bd9e914501a567a56e3ae64fa4d9c8dfff298618e9409d1156/mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b", size = 9567604 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802 }, +] + +[[package]] +name = "ruff" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/82271b5ecbb72f24178eac28979380c4ba234f90be5cf92cb513605efb1a/ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436", size = 2457325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d1/ac5091efcc8e2cdc55733ac07f17f961465318a3fa8916e44360e32e6c73/ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9", size = 9610279 }, + { url = "https://files.pythonhosted.org/packages/2b/ed/c3e1c20e46f5619f133e1ddafbb1a957407ea36d42a477d0d88e9897bed9/ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae", size = 8719541 }, + { url = "https://files.pythonhosted.org/packages/13/49/3ee1c8dca59a8bd87ca833871d86304bce4348b2e019287e45ca0ad5b3dd/ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850", size = 8320291 }, + { url = "https://files.pythonhosted.org/packages/2a/44/1fec4c3eac790a445f3b9e0759665439c1d88517851f3fca90e32e897d48/ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf", size = 10040885 }, + { url = "https://files.pythonhosted.org/packages/86/98/c0b96dda4f751accecd3c0638d8c617a3b3e6de11b4e68aa77cae72912fb/ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9", size = 9414183 }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59ac3b2fb4e80f53a96f2c22951589357e22ef3bc2c2b04b2a73772663f8/ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5", size = 10203467 }, + { url = "https://files.pythonhosted.org/packages/8d/02/3dc1c33877d68341b9764b30e2dcc9209b6adb8a0a41ca04d503dc39006e/ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024", size = 10962198 }, + { url = "https://files.pythonhosted.org/packages/c5/1f/a36bb06c8b724e3a8ee59124657414182227a353a98408cb5321aa87bd13/ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18", size = 10537682 }, + { url = "https://files.pythonhosted.org/packages/a2/bd/479fbfab1634f2527a3f5ddb44973977f75ffbdf3d9bb16748c558a263ad/ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e", size = 11505616 }, + { url = "https://files.pythonhosted.org/packages/e0/94/92bc24e7e58d2f90fa2a370f763d25d9e06ccccfab839b88e389d79fb4e3/ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1", size = 10221898 }, + { url = "https://files.pythonhosted.org/packages/f7/47/1aca18f02abd4a3ba739991b719a3aa5d8e39e0bee1a91090c8bfacdcd13/ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631", size = 10033784 }, + { url = "https://files.pythonhosted.org/packages/e6/48/df16d9b00af42034ee85915914783bc0529a2ff709d6d3ef39c7c15d826d/ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9", size = 9477381 }, + { url = "https://files.pythonhosted.org/packages/46/d6/d6eadedcc9f9c4927665eee26f4449c15f4c501e7ba9c34c37753748dc11/ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15", size = 9862269 }, + { url = "https://files.pythonhosted.org/packages/4e/30/e2f5b06ac048898a1cac190e1c9c0d88f984596b27f1069341217e42d119/ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb", size = 10287591 }, + { url = "https://files.pythonhosted.org/packages/a0/2c/6a17be1b3c69c03167e5b3d69317ae9b8b2a06091189161751e7a36afef5/ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4", size = 7918031 }, + { url = "https://files.pythonhosted.org/packages/2e/ba/66a6c87f6532e0390ebc67d5ae9bc1064f4e14d1b0e224bdedc999ae2b15/ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b", size = 8736178 }, + { url = "https://files.pythonhosted.org/packages/14/da/418c5d40058ad56bd0fa060efa4580ccf446f916167aa6540d31f6844e16/ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014", size = 8142791 }, +] + +[[package]] +name = "setuptools" +version = "73.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/f4d4ce9bc15e61edba3179f9b0f763fc6d439474d28511b11f0d95bab7a2/setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193", size = 2526506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6a/0270e295bf30c37567736b7fca10167640898214ff911273af37ddb95770/setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e", size = 2346588 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +]