├── .python-version ├── kademlia ├── tests │ ├── __init__.py │ ├── test_utils.py │ ├── test_storage.py │ ├── conftest.py │ ├── test_node.py │ ├── test_server.py │ └── test_routing.py ├── __init__.py ├── utils.py ├── storage.py ├── node.py ├── protocol.py ├── crawling.py ├── routing.py └── network.py ├── docs ├── requirements.txt ├── source │ ├── modules.rst │ └── kademlia.rst ├── querying.rst ├── intro.rst ├── index.rst ├── Makefile └── conf.py ├── .github ├── FUNDING.yml ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── pytest.ini ├── .gitignore ├── .readthedocs.yaml ├── examples ├── set.py ├── get.py └── node.py ├── LICENSE ├── pyproject.toml ├── CHANGELOG.md ├── README.md └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /kademlia/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests live here. 3 | """ 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.4.7 2 | sphinx-rtd-theme==3.0.2 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | liberapay: bmuller 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -vv --cov-report term-missing --cov kademlia 3 | asyncio_mode=auto 4 | asyncio_default_fixture_loop_scope="function" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pickle 2 | *.pid 3 | _trial_temp 4 | apidoc 5 | *.pyc 6 | build 7 | dist 8 | kademlia.egg-info 9 | docs/_build 10 | .coverage 11 | .coverage.* -------------------------------------------------------------------------------- /kademlia/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kademlia is a Python implementation of the Kademlia protocol which 3 | utilizes the asyncio library. 4 | """ 5 | 6 | __version__ = "2.2.3" 7 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Kademlia API 2 | ============ 3 | .. automodule:: kademlia 4 | 5 | The best place to start is the examples folder before diving into the API. 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | kademlia 11 | -------------------------------------------------------------------------------- /docs/querying.rst: -------------------------------------------------------------------------------- 1 | Querying the DHT 2 | ================== 3 | 4 | If you just want to query the network, you can use the example query script. For instance:: 5 | 6 | $ python examples/get.py 1.2.3.4 8468 SpecialKey 7 | 8 | The query script is simple: 9 | 10 | .. literalinclude:: ../examples/get.py 11 | 12 | Check out the examples folder for other examples. 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | # Set the OS, Python version and other tools you might need 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.12" 9 | 10 | # Build documentation in the "docs/" directory with Sphinx 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /kademlia/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from kademlia.utils import digest, shared_prefix 4 | 5 | 6 | class TestUtils: 7 | def test_digest(self): 8 | dig = hashlib.sha1(b"1").digest() 9 | assert dig == digest(1) 10 | 11 | dig = hashlib.sha1(b"another").digest() 12 | assert dig == digest("another") 13 | 14 | def test_shared_prefix(self): 15 | args = ["prefix", "prefixasdf", "prefix", "prefixxxx"] 16 | assert shared_prefix(args) == "prefix" 17 | 18 | args = ["p", "prefixasdf", "prefix", "prefixxxx"] 19 | assert shared_prefix(args) == "p" 20 | 21 | args = ["one", "two"] 22 | assert shared_prefix(args) == "" 23 | 24 | args = ["hi"] 25 | assert shared_prefix(args) == "hi" 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] 10 | name: Python ${{ matrix.python-version }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: astral-sh/setup-uv@v5 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | enable-cache: true 17 | cache-dependency-glob: "uv.lock" 18 | - name: Install the project 19 | run: uv sync --all-extras --dev 20 | - name: Check format 21 | run: uv run ruff format --check 22 | - name: Lint 23 | run: uv run ruff check --output-format=github . 24 | - name: Tests 25 | run: uv run pytest 26 | -------------------------------------------------------------------------------- /examples/set.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | 5 | from kademlia.network import Server 6 | 7 | if len(sys.argv) != 5: 8 | print("Usage: python set.py ") 9 | sys.exit(1) 10 | 11 | handler = logging.StreamHandler() 12 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 13 | handler.setFormatter(formatter) 14 | log = logging.getLogger("kademlia") 15 | log.addHandler(handler) 16 | log.setLevel(logging.DEBUG) 17 | 18 | 19 | async def run(): 20 | server = Server() 21 | await server.listen(8469) 22 | bootstrap_node = (sys.argv[1], int(sys.argv[2])) 23 | await server.bootstrap([bootstrap_node]) 24 | await server.set(sys.argv[3], sys.argv[4]) 25 | server.stop() 26 | 27 | 28 | asyncio.run(run()) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create an issue report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. OSX] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | - Python Version [e.g. 3.6.6] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /examples/get.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | 5 | from kademlia.network import Server 6 | 7 | if len(sys.argv) != 4: 8 | print("Usage: python get.py ") 9 | sys.exit(1) 10 | 11 | handler = logging.StreamHandler() 12 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 13 | handler.setFormatter(formatter) 14 | log = logging.getLogger("kademlia") 15 | log.addHandler(handler) 16 | log.setLevel(logging.DEBUG) 17 | 18 | 19 | async def run(): 20 | server = Server() 21 | await server.listen(8469) 22 | bootstrap_node = (sys.argv[1], int(sys.argv[2])) 23 | await server.bootstrap([bootstrap_node]) 24 | 25 | result = await server.get(sys.argv[3]) 26 | print("Get result:", result) 27 | server.stop() 28 | 29 | 30 | asyncio.run(run()) 31 | -------------------------------------------------------------------------------- /kademlia/tests/test_storage.py: -------------------------------------------------------------------------------- 1 | from kademlia.storage import ForgetfulStorage 2 | 3 | 4 | class ForgetfulStorageTest: 5 | def test_storing(self): 6 | storage = ForgetfulStorage(10) 7 | storage["one"] = "two" 8 | assert storage["one"] == "two" 9 | 10 | def test_forgetting(self): 11 | storage = ForgetfulStorage(0) 12 | storage["one"] = "two" 13 | assert storage.get("one") is None 14 | 15 | def test_iter(self): 16 | storage = ForgetfulStorage(10) 17 | storage["one"] = "two" 18 | for key, value in storage: 19 | assert key == "one" 20 | assert value == "two" 21 | 22 | def test_iter_old(self): 23 | storage = ForgetfulStorage(10) 24 | storage["one"] = "two" 25 | for key, value in storage.iter_older_than(0): 26 | assert key == "one" 27 | assert value == "two" 28 | -------------------------------------------------------------------------------- /kademlia/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | General catchall for functions that don't make sense as methods. 3 | """ 4 | 5 | import asyncio 6 | import hashlib 7 | import operator 8 | 9 | 10 | async def gather_dict(dic): 11 | cors = list(dic.values()) 12 | results = await asyncio.gather(*cors) 13 | return dict(zip(dic.keys(), results)) 14 | 15 | 16 | def digest(string): 17 | if not isinstance(string, bytes): 18 | string = str(string).encode("utf8") 19 | return hashlib.sha1(string).digest() 20 | 21 | 22 | def shared_prefix(args): 23 | """ 24 | Find the shared prefix between the strings. 25 | 26 | For instance: 27 | 28 | sharedPrefix(['blahblah', 'blahwhat']) 29 | 30 | returns 'blah'. 31 | """ 32 | i = 0 33 | while i < min(map(len, args)): 34 | if len(set(map(operator.itemgetter(i), args))) != 1: 35 | break 36 | i += 1 37 | return args[0][:i] 38 | 39 | 40 | def bytes_to_bit_string(bites): 41 | bits = [bin(bite)[2:].rjust(8, "0") for bite in bites] 42 | return "".join(bits) 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Brian Muller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ================== 3 | 4 | The easiest (and best) way to install kademlia is through `pip `_:: 5 | 6 | $ pip install kademlia 7 | 8 | 9 | Usage 10 | ===== 11 | To start a new network, create the first node. Future nodes will connect to this first node (and any other nodes you know about) to create the network. 12 | 13 | .. literalinclude:: ../examples/node.py 14 | 15 | Here's an example of bootstrapping a new node against a known node and then setting a value: 16 | 17 | .. literalinclude:: ../examples/set.py 18 | 19 | .. note :: 20 | You must have at least two nodes running to store values. If a node tries to store a value and there are no other nodes to provide redundancy, then it is an exception state. 21 | 22 | 23 | Running Tests 24 | ============= 25 | 26 | To run tests:: 27 | 28 | $ pip install -r dev-requirements.txt 29 | $ pytest 30 | 31 | 32 | Fidelity to Original Paper 33 | ========================== 34 | The current implementation should be an accurate implementation of all aspects of the paper except one - in Section 2.3 there is the requirement that the original publisher of a key/value republish it every 24 hours. This library does not do this (though you can easily do this manually). 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Kademlia documentation master file, created by 2 | sphinx-quickstart on Mon Jan 5 09:42:46 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Kademlia Documentation 7 | ====================== 8 | 9 | .. note :: 10 | This library assumes you have a working familiarity with asyncio_. 11 | 12 | This library is an asynchronous Python implementation of the `Kademlia distributed hash table `_. It uses asyncio_ to provide asynchronous communication. The nodes communicate using `RPC over UDP `_ to communiate, meaning that it is capable of working behind a `NAT `_. 13 | 14 | This library aims to be as close to a reference implementation of the `Kademlia paper `_ as possible. 15 | 16 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 17 | 18 | .. toctree:: 19 | :maxdepth: 3 20 | :titlesonly: 21 | 22 | intro 23 | querying 24 | source/modules 25 | 26 | 27 | Indices and tables 28 | ================== 29 | 30 | * :ref:`genindex` 31 | * :ref:`modindex` 32 | * :ref:`search` 33 | 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "kademlia" 3 | dynamic = ["version"] 4 | description = "Kademlia is a distributed hash table for decentralized peer-to-peer computer networks." 5 | readme = "README.md" 6 | authors = [ 7 | { name = "Brian Muller", email = "bamuller@gmail.com" } 8 | ] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | "Programming Language :: Python", 15 | "Topic :: Software Development :: Libraries :: Python Modules", 16 | ] 17 | requires-python = ">=3.9" 18 | dependencies = [ 19 | "rpcudp>=5.0.1", 20 | ] 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/bmuller/kademlia" 24 | Issues = "https://github.com/bmuller/kademlia/issues" 25 | 26 | [build-system] 27 | requires = ["hatchling"] 28 | build-backend = "hatchling.build" 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["kademlia"] 32 | 33 | [tool.hatch.version] 34 | path = "kademlia/__init__.py" 35 | 36 | [tool.ruff.lint] 37 | select = ["E", "F", "UP", "B", "SIM", "I"] 38 | 39 | [dependency-groups] 40 | dev = [ 41 | "pytest>=8.3.5", 42 | "pytest-asyncio>=0.26.0", 43 | "pytest-cov>=6.0.0", 44 | "ruff>=0.11.2", 45 | ] 46 | docs = [ 47 | "sphinx>=7.4.7", 48 | "sphinx-rtd-theme>=3.0.2", 49 | ] 50 | -------------------------------------------------------------------------------- /docs/source/kademlia.rst: -------------------------------------------------------------------------------- 1 | kademlia package 2 | ================ 3 | 4 | The best place to start is the examples folder before diving into the API. 5 | 6 | kademlia.crawling module 7 | ------------------------ 8 | 9 | .. automodule:: kademlia.crawling 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | 14 | kademlia.network module 15 | ----------------------- 16 | 17 | .. automodule:: kademlia.network 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | kademlia.node module 23 | -------------------- 24 | 25 | .. automodule:: kademlia.node 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | kademlia.protocol module 31 | ------------------------ 32 | 33 | .. automodule:: kademlia.protocol 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | kademlia.routing module 39 | ----------------------- 40 | 41 | .. automodule:: kademlia.routing 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | kademlia.storage module 47 | ----------------------- 48 | 49 | .. automodule:: kademlia.storage 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | :special-members: __getitem__, __setitem__ 54 | 55 | kademlia.utils module 56 | --------------------- 57 | 58 | .. automodule:: kademlia.utils 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | -------------------------------------------------------------------------------- /kademlia/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import random 3 | from struct import pack 4 | 5 | import pytest 6 | 7 | from kademlia.network import Server 8 | from kademlia.node import Node 9 | from kademlia.routing import RoutingTable 10 | 11 | 12 | @pytest.fixture() 13 | def bootstrap_node(event_loop): 14 | server = Server() 15 | event_loop.run_until_complete(server.listen(8468)) 16 | 17 | try: 18 | yield ("127.0.0.1", 8468) 19 | finally: 20 | server.stop() 21 | 22 | 23 | @pytest.fixture() 24 | def mknode(): 25 | def _mknode(node_id=None, ip_addy=None, port=None, intid=None): 26 | """ 27 | Make a node. Created a random id if not specified. 28 | """ 29 | if intid is not None: 30 | node_id = pack(">l", intid) 31 | if not node_id: 32 | randbits = str(random.getrandbits(255)) 33 | node_id = hashlib.sha1(randbits.encode()).digest() 34 | return Node(node_id, ip_addy, port) 35 | 36 | return _mknode 37 | 38 | 39 | class FakeProtocol: 40 | def __init__(self, source_id, ksize=20): 41 | self.router = RoutingTable(self, ksize, Node(source_id)) 42 | self.storage = {} 43 | self.source_id = source_id 44 | 45 | 46 | class FakeServer: 47 | def __init__(self, node_id): 48 | self.id = node_id 49 | self.protocol = FakeProtocol(self.id) 50 | self.router = self.protocol.router 51 | 52 | 53 | @pytest.fixture 54 | def fake_server(mknode): 55 | return FakeServer(mknode().id) 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## Version 2.2.3 (2025-03-30) 7 | ### Enhancements 8 | * Moved to `uv` for dependency management and `ruff` for formatting / styling / linting 9 | 10 | ## Verson 2.2.2 (2021-02-04) 11 | 12 | ### Enhancements 13 | * Added a MANIFEST.in so the source distribution can retain necessary requirements.txt file 14 | 15 | ## Verson 2.2.1 (2020-05-02) 16 | 17 | ### Enhancements 18 | * update tests without unittest only pytest (#62) 19 | * added additional docs to `Node` to reduce confusion noted in #73 20 | 21 | ### Bug Fixes 22 | * Fixed unexpected type conversion in buckets (#77) 23 | * Fixed issue with load_state not awaiting bootstrap (#78) 24 | * Fixed `KBucket.replacement_nodes` is never pruned (#79) 25 | 26 | ## Verson 2.2 (2019-02-04) 27 | minor version update to handle long_description_content_type for pypi 28 | 29 | ## Verson 2.1 (2019-02-04) 30 | 31 | ### Bug Fixes 32 | * `KBucket.remove_node` removes nodes in replacement_nodes. (#66) 33 | * Improve `KBucket.split` (#65) 34 | * refacto(storage): use '@abstractmethod' instead of 'raise NotImplementedError' in Storage Interface (#61) 35 | * Asynchronous Server listening (#58) (#60) 36 | 37 | ## Version 2.0 (2019-01-09) 38 | 39 | ### Bug Fixes 40 | * Removed unused imports 41 | 42 | ### Deprecations 43 | * Removed all camelcase naming 44 | -------------------------------------------------------------------------------- /kademlia/tests/test_node.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import random 3 | 4 | from kademlia.node import Node, NodeHeap 5 | 6 | 7 | class TestNode: 8 | def test_long_id(self): 9 | rid = hashlib.sha1(str(random.getrandbits(255)).encode()).digest() 10 | node = Node(rid) 11 | assert node.long_id == int(rid.hex(), 16) 12 | 13 | def test_distance_calculation(self): 14 | ridone = hashlib.sha1(str(random.getrandbits(255)).encode()) 15 | ridtwo = hashlib.sha1(str(random.getrandbits(255)).encode()) 16 | 17 | shouldbe = int(ridone.hexdigest(), 16) ^ int(ridtwo.hexdigest(), 16) 18 | none = Node(ridone.digest()) 19 | ntwo = Node(ridtwo.digest()) 20 | assert none.distance_to(ntwo) == shouldbe 21 | 22 | 23 | class TestNodeHeap: 24 | def test_max_size(self, mknode): 25 | node = NodeHeap(mknode(intid=0), 3) 26 | assert not node 27 | 28 | for digit in range(10): 29 | node.push(mknode(intid=digit)) 30 | 31 | assert len(node) == 3 32 | assert len(list(node)) == 3 33 | 34 | def test_iteration(self, mknode): 35 | heap = NodeHeap(mknode(intid=0), 5) 36 | nodes = [mknode(intid=x) for x in range(10)] 37 | for _index, node in enumerate(nodes): 38 | heap.push(node) 39 | for index, node in enumerate(heap): 40 | assert index == node.long_id 41 | assert index < 5 42 | 43 | def test_remove(self, mknode): 44 | heap = NodeHeap(mknode(intid=0), 5) 45 | nodes = [mknode(intid=x) for x in range(10)] 46 | for node in nodes: 47 | heap.push(node) 48 | 49 | heap.remove([nodes[0].id, nodes[1].id]) 50 | assert len(list(heap)) == 5 51 | for index, node in enumerate(heap): 52 | assert index + 2 == node.long_id 53 | assert index < 5 54 | -------------------------------------------------------------------------------- /examples/node.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import logging 4 | 5 | from kademlia.network import Server 6 | 7 | handler = logging.StreamHandler() 8 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 9 | handler.setFormatter(formatter) 10 | log = logging.getLogger("kademlia") 11 | log.addHandler(handler) 12 | log.setLevel(logging.DEBUG) 13 | 14 | server = Server() 15 | 16 | 17 | def parse_arguments(): 18 | parser = argparse.ArgumentParser() 19 | 20 | # Optional arguments 21 | parser.add_argument( 22 | "-i", "--ip", help="IP address of existing node", type=str, default=None 23 | ) 24 | parser.add_argument( 25 | "-p", "--port", help="port number of existing node", type=int, default=None 26 | ) 27 | 28 | return parser.parse_args() 29 | 30 | 31 | def connect_to_bootstrap_node(args): 32 | loop = asyncio.get_event_loop() 33 | loop.set_debug(True) 34 | 35 | loop.run_until_complete(server.listen(8469)) 36 | bootstrap_node = (args.ip, int(args.port)) 37 | loop.run_until_complete(server.bootstrap([bootstrap_node])) 38 | 39 | try: 40 | loop.run_forever() 41 | except KeyboardInterrupt: 42 | pass 43 | finally: 44 | server.stop() 45 | loop.close() 46 | 47 | 48 | def create_bootstrap_node(): 49 | loop = asyncio.get_event_loop() 50 | loop.set_debug(True) 51 | 52 | loop.run_until_complete(server.listen(8468)) 53 | 54 | try: 55 | loop.run_forever() 56 | except KeyboardInterrupt: 57 | pass 58 | finally: 59 | server.stop() 60 | loop.close() 61 | 62 | 63 | def main(): 64 | args = parse_arguments() 65 | 66 | if args.ip and args.port: 67 | connect_to_bootstrap_node(args) 68 | else: 69 | create_bootstrap_node() 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /kademlia/tests/test_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from kademlia.network import Server 6 | from kademlia.protocol import KademliaProtocol 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_storing(bootstrap_node): 11 | server = Server() 12 | await server.listen(bootstrap_node[1] + 1) 13 | await server.bootstrap([bootstrap_node]) 14 | await server.set("key", "value") 15 | result = await server.get("key") 16 | 17 | assert result == "value" 18 | 19 | server.stop() 20 | 21 | 22 | class TestSwappableProtocol: 23 | def test_default_protocol(self): 24 | """ 25 | An ordinary Server object will initially not have a protocol, but will 26 | have a KademliaProtocol object as its protocol after its listen() 27 | method is called. 28 | """ 29 | loop = asyncio.get_event_loop() 30 | server = Server() 31 | assert server.protocol is None 32 | loop.run_until_complete(server.listen(8469)) 33 | assert isinstance(server.protocol, KademliaProtocol) 34 | server.stop() 35 | 36 | def test_custom_protocol(self): 37 | """ 38 | A subclass of Server which overrides the protocol_class attribute will 39 | have an instance of that class as its protocol after its listen() 40 | method is called. 41 | """ 42 | 43 | # Make a custom Protocol and Server to go with hit. 44 | class CoconutProtocol(KademliaProtocol): 45 | pass 46 | 47 | class HuskServer(Server): 48 | protocol_class = CoconutProtocol 49 | 50 | # An ordinary server does NOT have a CoconutProtocol as its protocol... 51 | loop = asyncio.get_event_loop() 52 | server = Server() 53 | loop.run_until_complete(server.listen(8469)) 54 | assert not isinstance(server.protocol, CoconutProtocol) 55 | server.stop() 56 | 57 | # ...but our custom server does. 58 | husk_server = HuskServer() 59 | loop.run_until_complete(husk_server.listen(8469)) 60 | assert isinstance(husk_server.protocol, CoconutProtocol) 61 | husk_server.stop() 62 | -------------------------------------------------------------------------------- /kademlia/storage.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import time 3 | from abc import ABC, abstractmethod 4 | from collections import OrderedDict 5 | from itertools import takewhile 6 | 7 | 8 | class IStorage(ABC): 9 | """ 10 | Local storage for this node. 11 | IStorage implementations of get must return the same type as put in by set 12 | """ 13 | 14 | @abstractmethod 15 | def __setitem__(self, key, value): 16 | """ 17 | Set a key to the given value. 18 | """ 19 | 20 | @abstractmethod 21 | def __getitem__(self, key): 22 | """ 23 | Get the given key. If item doesn't exist, raises C{KeyError} 24 | """ 25 | 26 | @abstractmethod 27 | def get(self, key, default=None): 28 | """ 29 | Get given key. If not found, return default. 30 | """ 31 | 32 | @abstractmethod 33 | def iter_older_than(self, seconds_old): 34 | """ 35 | Return the an iterator over (key, value) tuples for items older 36 | than the given secondsOld. 37 | """ 38 | 39 | @abstractmethod 40 | def __iter__(self): 41 | """ 42 | Get the iterator for this storage, should yield tuple of (key, value) 43 | """ 44 | 45 | 46 | class ForgetfulStorage(IStorage): 47 | def __init__(self, ttl=604800): 48 | """ 49 | By default, max age is a week. 50 | """ 51 | self.data = OrderedDict() 52 | self.ttl = ttl 53 | 54 | def __setitem__(self, key, value): 55 | if key in self.data: 56 | del self.data[key] 57 | self.data[key] = (time.monotonic(), value) 58 | self.cull() 59 | 60 | def cull(self): 61 | for _, _ in self.iter_older_than(self.ttl): 62 | self.data.popitem(last=False) 63 | 64 | def get(self, key, default=None): 65 | self.cull() 66 | if key in self.data: 67 | return self[key] 68 | return default 69 | 70 | def __getitem__(self, key): 71 | self.cull() 72 | return self.data[key][1] 73 | 74 | def __repr__(self): 75 | self.cull() 76 | return repr(self.data) 77 | 78 | def iter_older_than(self, seconds_old): 79 | min_birthday = time.monotonic() - seconds_old 80 | zipped = self._triple_iter() 81 | matches = takewhile(lambda r: min_birthday >= r[1], zipped) 82 | return list(map(operator.itemgetter(0, 2), matches)) 83 | 84 | def _triple_iter(self): 85 | ikeys = self.data.keys() 86 | ibirthday = map(operator.itemgetter(0), self.data.values()) 87 | ivalues = map(operator.itemgetter(1), self.data.values()) 88 | return zip(ikeys, ibirthday, ivalues) 89 | 90 | def __iter__(self): 91 | self.cull() 92 | ikeys = self.data.keys() 93 | ivalues = map(operator.itemgetter(1), self.data.values()) 94 | return zip(ikeys, ivalues) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Distributed Hash Table 2 | [![Build Status](https://github.com/bmuller/kademlia/actions/workflows/ci.yml/badge.svg)](https://github.com/bmuller/kademlia/actions/workflows/ci.yml) 3 | [![Docs Status](https://readthedocs.org/projects/kademlia/badge/?version=latest)](http://kademlia.readthedocs.org) 4 | 5 | **Documentation can be found at [kademlia.readthedocs.org](http://kademlia.readthedocs.org/).** 6 | 7 | This library is an asynchronous Python implementation of the [Kademlia distributed hash table](http://en.wikipedia.org/wiki/Kademlia). It uses the [asyncio library](https://docs.python.org/3/library/asyncio.html) in Python 3 to provide asynchronous communication. The nodes communicate using [RPC over UDP](https://github.com/bmuller/rpcudp) to communiate, meaning that it is capable of working behind a [NAT](http://en.wikipedia.org/wiki/Network_address_translation). 8 | 9 | This library aims to be as close to a reference implementation of the [Kademlia paper](http://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) as possible. 10 | 11 | ## Installation 12 | 13 | ``` 14 | pip install kademlia 15 | ``` 16 | 17 | ## Usage 18 | *This assumes you have a working familiarity with [asyncio](https://docs.python.org/3/library/asyncio.html).* 19 | 20 | Assuming you want to connect to an existing network: 21 | 22 | ```python 23 | import asyncio 24 | from kademlia.network import Server 25 | 26 | async def run(): 27 | # Create a node and start listening on port 5678 28 | node = Server() 29 | await node.listen(5678) 30 | 31 | # Bootstrap the node by connecting to other known nodes, in this case 32 | # replace 123.123.123.123 with the IP of another node and optionally 33 | # give as many ip/port combos as you can for other nodes. 34 | await node.bootstrap([("123.123.123.123", 5678)]) 35 | 36 | # set a value for the key "my-key" on the network 37 | await node.set("my-key", "my awesome value") 38 | 39 | # get the value associated with "my-key" from the network 40 | result = await node.get("my-key") 41 | print(result) 42 | 43 | asyncio.run(run()) 44 | ``` 45 | 46 | ## Initializing a Network 47 | If you're starting a new network from scratch, just omit the `node.bootstrap` call in the example above. Then, bootstrap other nodes by connecting to the first node you started. 48 | 49 | See the examples folder for a first node example that other nodes can bootstrap connect to and some code that gets and sets a key/value. 50 | 51 | ## Logging 52 | This library uses the standard [Python logging library](https://docs.python.org/3/library/logging.html). To see debut output printed to STDOUT, for instance, use: 53 | 54 | ```python 55 | import logging 56 | 57 | log = logging.getLogger('kademlia') 58 | log.setLevel(logging.DEBUG) 59 | log.addHandler(logging.StreamHandler()) 60 | ``` 61 | 62 | ## Running Tests 63 | To run tests: 64 | 65 | ``` 66 | uv sync --dev 67 | uv run pytest 68 | ``` 69 | 70 | ## Reporting Issues 71 | Please report all issues [on github](https://github.com/bmuller/kademlia/issues). 72 | 73 | ## Fidelity to Original Paper 74 | The current implementation should be an accurate implementation of all aspects of the paper save one - in Section 2.3 there is the requirement that the original publisher of a key/value republish it every 24 hours. This library does not do this (though you can easily do this manually). 75 | -------------------------------------------------------------------------------- /kademlia/node.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | from operator import itemgetter 3 | 4 | 5 | class Node: 6 | """ 7 | Simple object to encapsulate the concept of a Node (minimally an ID, but 8 | also possibly an IP and port if this represents a node on the network). 9 | This class should generally not be instantiated directly, as it is a low 10 | level construct mostly used by the router. 11 | """ 12 | 13 | def __init__(self, node_id, ip=None, port=None): 14 | """ 15 | Create a Node instance. 16 | 17 | Args: 18 | node_id (int): A value between 0 and 2^160 19 | ip (string): Optional IP address where this Node lives 20 | port (int): Optional port for this Node (set when IP is set) 21 | """ 22 | self.id = node_id 23 | self.ip = ip 24 | self.port = port 25 | self.long_id = int(node_id.hex(), 16) 26 | 27 | def same_home_as(self, node): 28 | return self.ip == node.ip and self.port == node.port 29 | 30 | def distance_to(self, node): 31 | """ 32 | Get the distance between this node and another. 33 | """ 34 | return self.long_id ^ node.long_id 35 | 36 | def __iter__(self): 37 | """ 38 | Enables use of Node as a tuple - i.e., tuple(node) works. 39 | """ 40 | return iter([self.id, self.ip, self.port]) 41 | 42 | def __repr__(self): 43 | return repr([self.long_id, self.ip, self.port]) 44 | 45 | def __str__(self): 46 | return f"{self.ip}:{self.port}" 47 | 48 | 49 | class NodeHeap: 50 | """ 51 | A heap of nodes ordered by distance to a given node. 52 | """ 53 | 54 | def __init__(self, node, maxsize): 55 | """ 56 | Constructor. 57 | 58 | @param node: The node to measure all distnaces from. 59 | @param maxsize: The maximum size that this heap can grow to. 60 | """ 61 | self.node = node 62 | self.heap = [] 63 | self.contacted = set() 64 | self.maxsize = maxsize 65 | 66 | def remove(self, peers): 67 | """ 68 | Remove a list of peer ids from this heap. Note that while this 69 | heap retains a constant visible size (based on the iterator), it's 70 | actual size may be quite a bit larger than what's exposed. Therefore, 71 | removal of nodes may not change the visible size as previously added 72 | nodes suddenly become visible. 73 | """ 74 | peers = set(peers) 75 | if not peers: 76 | return 77 | nheap = [] 78 | for distance, node in self.heap: 79 | if node.id not in peers: 80 | heapq.heappush(nheap, (distance, node)) 81 | self.heap = nheap 82 | 83 | def get_node(self, node_id): 84 | for _, node in self.heap: 85 | if node.id == node_id: 86 | return node 87 | return None 88 | 89 | def have_contacted_all(self): 90 | return len(self.get_uncontacted()) == 0 91 | 92 | def get_ids(self): 93 | return [n.id for n in self] 94 | 95 | def mark_contacted(self, node): 96 | self.contacted.add(node.id) 97 | 98 | def popleft(self): 99 | return heapq.heappop(self.heap)[1] if self else None 100 | 101 | def push(self, nodes): 102 | """ 103 | Push nodes onto heap. 104 | 105 | @param nodes: This can be a single item or a C{list}. 106 | """ 107 | if not isinstance(nodes, list): 108 | nodes = [nodes] 109 | 110 | for node in nodes: 111 | if node not in self: 112 | distance = self.node.distance_to(node) 113 | heapq.heappush(self.heap, (distance, node)) 114 | 115 | def __len__(self): 116 | return min(len(self.heap), self.maxsize) 117 | 118 | def __iter__(self): 119 | nodes = heapq.nsmallest(self.maxsize, self.heap) 120 | return iter(map(itemgetter(1), nodes)) 121 | 122 | def __contains__(self, node): 123 | return any(node.id == other.id for _, other in self.heap) 124 | 125 | def get_uncontacted(self): 126 | return [n for n in self if n.id not in self.contacted] 127 | -------------------------------------------------------------------------------- /kademlia/tests/test_routing.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | 3 | from kademlia.routing import KBucket, TableTraverser 4 | 5 | 6 | class TestKBucket: 7 | def test_split(self, mknode): 8 | bucket = KBucket(0, 10, 5) 9 | bucket.add_node(mknode(intid=5)) 10 | bucket.add_node(mknode(intid=6)) 11 | one, two = bucket.split() 12 | assert len(one) == 1 13 | assert one.range == (0, 5) 14 | assert len(two) == 1 15 | assert two.range == (6, 10) 16 | 17 | def test_split_no_overlap(self): 18 | left, right = KBucket(0, 2**160, 20).split() 19 | assert (right.range[0] - left.range[1]) == 1 20 | 21 | def test_add_node(self, mknode): 22 | # when full, return false 23 | bucket = KBucket(0, 10, 2) 24 | assert bucket.add_node(mknode()) is True 25 | assert bucket.add_node(mknode()) is True 26 | assert bucket.add_node(mknode()) is False 27 | assert len(bucket) == 2 28 | 29 | # make sure when a node is double added it's put at the end 30 | bucket = KBucket(0, 10, 3) 31 | nodes = [mknode(), mknode(), mknode()] 32 | for node in nodes: 33 | bucket.add_node(node) 34 | for index, node in enumerate(bucket.get_nodes()): 35 | assert node == nodes[index] 36 | 37 | def test_remove_node(self, mknode): 38 | k = 3 39 | bucket = KBucket(0, 10, k) 40 | nodes = [mknode() for _ in range(10)] 41 | for node in nodes: 42 | bucket.add_node(node) 43 | 44 | replacement_nodes = bucket.replacement_nodes 45 | assert list(bucket.nodes.values()) == nodes[:k] 46 | assert list(replacement_nodes.values()) == nodes[k:] 47 | 48 | bucket.remove_node(nodes.pop()) 49 | assert list(bucket.nodes.values()) == nodes[:k] 50 | assert list(replacement_nodes.values()) == nodes[k:] 51 | 52 | bucket.remove_node(nodes.pop(0)) 53 | assert list(bucket.nodes.values()) == nodes[: k - 1] + nodes[-1:] 54 | assert list(replacement_nodes.values()) == nodes[k - 1 : -1] 55 | 56 | shuffle(nodes) 57 | for node in nodes: 58 | bucket.remove_node(node) 59 | assert not bucket 60 | assert not replacement_nodes 61 | 62 | def test_in_range(self, mknode): 63 | bucket = KBucket(0, 10, 10) 64 | assert bucket.has_in_range(mknode(intid=5)) is True 65 | assert bucket.has_in_range(mknode(intid=11)) is False 66 | assert bucket.has_in_range(mknode(intid=10)) is True 67 | assert bucket.has_in_range(mknode(intid=0)) is True 68 | 69 | def test_replacement_factor(self, mknode): 70 | k = 3 71 | factor = 2 72 | bucket = KBucket(0, 10, k, replacementNodeFactor=factor) 73 | nodes = [mknode() for _ in range(10)] 74 | for node in nodes: 75 | bucket.add_node(node) 76 | 77 | replacement_nodes = bucket.replacement_nodes 78 | assert len(list(replacement_nodes.values())) == k * factor 79 | assert list(replacement_nodes.values()) == nodes[k + 1 :] 80 | assert nodes[k] not in list(replacement_nodes.values()) 81 | 82 | 83 | class TestRoutingTable: 84 | def test_add_contact(self, fake_server, mknode): 85 | fake_server.router.add_contact(mknode()) 86 | assert len(fake_server.router.buckets) == 1 87 | assert len(fake_server.router.buckets[0].nodes) == 1 88 | 89 | 90 | class TestTableTraverser: 91 | def test_iteration(self, fake_server, mknode): 92 | """ 93 | Make 10 nodes, 5 buckets, two nodes add to one bucket in order, 94 | All buckets: [node0, node1], [node2, node3], [node4, node5], 95 | [node6, node7], [node8, node9] 96 | Test traver result starting from node4. 97 | """ 98 | 99 | nodes = [mknode(intid=x) for x in range(10)] 100 | 101 | buckets = [] 102 | for i in range(5): 103 | bucket = KBucket(2 * i, 2 * i + 1, 2) 104 | bucket.add_node(nodes[2 * i]) 105 | bucket.add_node(nodes[2 * i + 1]) 106 | buckets.append(bucket) 107 | 108 | # replace router's bucket with our test buckets 109 | fake_server.router.buckets = buckets 110 | 111 | # expected nodes order 112 | expected_nodes = [ 113 | nodes[5], 114 | nodes[4], 115 | nodes[3], 116 | nodes[2], 117 | nodes[7], 118 | nodes[6], 119 | nodes[1], 120 | nodes[0], 121 | nodes[9], 122 | nodes[8], 123 | ] 124 | 125 | start_node = nodes[4] 126 | table_traverser = TableTraverser(fake_server.router, start_node) 127 | for index, node in enumerate(table_traverser): 128 | assert node == expected_nodes[index] 129 | -------------------------------------------------------------------------------- /kademlia/protocol.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import random 4 | 5 | from rpcudp.protocol import RPCProtocol 6 | 7 | from kademlia.node import Node 8 | from kademlia.routing import RoutingTable 9 | from kademlia.utils import digest 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class KademliaProtocol(RPCProtocol): 15 | def __init__(self, source_node, storage, ksize): 16 | RPCProtocol.__init__(self) 17 | self.router = RoutingTable(self, ksize, source_node) 18 | self.storage = storage 19 | self.source_node = source_node 20 | 21 | def get_refresh_ids(self): 22 | """ 23 | Get ids to search for to keep old buckets up to date. 24 | """ 25 | ids = [] 26 | for bucket in self.router.lonely_buckets(): 27 | rid = random.randint(*bucket.range).to_bytes(20, byteorder="big") 28 | ids.append(rid) 29 | return ids 30 | 31 | def rpc_stun(self, sender): 32 | return sender 33 | 34 | def rpc_ping(self, sender, nodeid): 35 | source = Node(nodeid, sender[0], sender[1]) 36 | self.welcome_if_new(source) 37 | return self.source_node.id 38 | 39 | def rpc_store(self, sender, nodeid, key, value): 40 | source = Node(nodeid, sender[0], sender[1]) 41 | self.welcome_if_new(source) 42 | log.debug( 43 | "got a store request from %s, storing '%s'='%s'", sender, key.hex(), value 44 | ) 45 | self.storage[key] = value 46 | return True 47 | 48 | def rpc_find_node(self, sender, nodeid, key): 49 | log.info("finding neighbors of %i in local table", int(nodeid.hex(), 16)) 50 | source = Node(nodeid, sender[0], sender[1]) 51 | self.welcome_if_new(source) 52 | node = Node(key) 53 | neighbors = self.router.find_neighbors(node, exclude=source) 54 | return list(map(tuple, neighbors)) 55 | 56 | def rpc_find_value(self, sender, nodeid, key): 57 | source = Node(nodeid, sender[0], sender[1]) 58 | self.welcome_if_new(source) 59 | value = self.storage.get(key, None) 60 | if value is None: 61 | return self.rpc_find_node(sender, nodeid, key) 62 | return {"value": value} 63 | 64 | async def call_find_node(self, node_to_ask, node_to_find): 65 | address = (node_to_ask.ip, node_to_ask.port) 66 | result = await self.find_node(address, self.source_node.id, node_to_find.id) 67 | return self.handle_call_response(result, node_to_ask) 68 | 69 | async def call_find_value(self, node_to_ask, node_to_find): 70 | address = (node_to_ask.ip, node_to_ask.port) 71 | result = await self.find_value(address, self.source_node.id, node_to_find.id) 72 | return self.handle_call_response(result, node_to_ask) 73 | 74 | async def call_ping(self, node_to_ask): 75 | address = (node_to_ask.ip, node_to_ask.port) 76 | result = await self.ping(address, self.source_node.id) 77 | return self.handle_call_response(result, node_to_ask) 78 | 79 | async def call_store(self, node_to_ask, key, value): 80 | address = (node_to_ask.ip, node_to_ask.port) 81 | result = await self.store(address, self.source_node.id, key, value) 82 | return self.handle_call_response(result, node_to_ask) 83 | 84 | def welcome_if_new(self, node): 85 | """ 86 | Given a new node, send it all the keys/values it should be storing, 87 | then add it to the routing table. 88 | 89 | @param node: A new node that just joined (or that we just found out 90 | about). 91 | 92 | Process: 93 | For each key in storage, get k closest nodes. If newnode is closer 94 | than the furtherst in that list, and the node for this server 95 | is closer than the closest in that list, then store the key/value 96 | on the new node (per section 2.5 of the paper) 97 | """ 98 | if not self.router.is_new_node(node): 99 | return 100 | 101 | log.info("never seen %s before, adding to router", node) 102 | for key, value in self.storage: 103 | keynode = Node(digest(key)) 104 | neighbors = self.router.find_neighbors(keynode) 105 | if neighbors: 106 | last = neighbors[-1].distance_to(keynode) 107 | new_node_close = node.distance_to(keynode) < last 108 | first = neighbors[0].distance_to(keynode) 109 | this_closest = self.source_node.distance_to(keynode) < first 110 | if not neighbors or (new_node_close and this_closest): 111 | asyncio.ensure_future(self.call_store(node, key, value)) 112 | self.router.add_contact(node) 113 | 114 | def handle_call_response(self, result, node): 115 | """ 116 | If we get a response, add the node to the routing table. If 117 | we get no response, make sure it's removed from the routing table. 118 | """ 119 | if not result[0]: 120 | log.warning("no response from %s, removing from router", node) 121 | self.router.remove_contact(node) 122 | return result 123 | 124 | log.info("got successful response from %s", node) 125 | self.welcome_if_new(node) 126 | return result 127 | -------------------------------------------------------------------------------- /kademlia/crawling.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import Counter 3 | 4 | from kademlia.node import Node, NodeHeap 5 | from kademlia.utils import gather_dict 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class SpiderCrawl: 11 | """ 12 | Crawl the network and look for given 160-bit keys. 13 | """ 14 | 15 | def __init__(self, protocol, node, peers, ksize, alpha): 16 | """ 17 | Create a new C{SpiderCrawl}er. 18 | 19 | Args: 20 | protocol: A :class:`~kademlia.protocol.KademliaProtocol` instance. 21 | node: A :class:`~kademlia.node.Node` representing the key we're 22 | looking for 23 | peers: A list of :class:`~kademlia.node.Node` instances that 24 | provide the entry point for the network 25 | ksize: The value for k based on the paper 26 | alpha: The value for alpha based on the paper 27 | """ 28 | self.protocol = protocol 29 | self.ksize = ksize 30 | self.alpha = alpha 31 | self.node = node 32 | self.nearest = NodeHeap(self.node, self.ksize) 33 | self.last_ids_crawled = [] 34 | log.info("creating spider with peers: %s", peers) 35 | self.nearest.push(peers) 36 | 37 | async def _find(self, rpcmethod): 38 | """ 39 | Get either a value or list of nodes. 40 | 41 | Args: 42 | rpcmethod: The protocol's callfindValue or call_find_node. 43 | 44 | The process: 45 | 1. calls find_* to current ALPHA nearest not already queried nodes, 46 | adding results to current nearest list of k nodes. 47 | 2. current nearest list needs to keep track of who has been queried 48 | already sort by nearest, keep KSIZE 49 | 3. if list is same as last time, next call should be to everyone not 50 | yet queried 51 | 4. repeat, unless nearest list has all been queried, then ur done 52 | """ 53 | log.info("crawling network with nearest: %s", str(tuple(self.nearest))) 54 | count = self.alpha 55 | if self.nearest.get_ids() == self.last_ids_crawled: 56 | count = len(self.nearest) 57 | self.last_ids_crawled = self.nearest.get_ids() 58 | 59 | dicts = {} 60 | for peer in self.nearest.get_uncontacted()[:count]: 61 | dicts[peer.id] = rpcmethod(peer, self.node) 62 | self.nearest.mark_contacted(peer) 63 | found = await gather_dict(dicts) 64 | return await self._nodes_found(found) 65 | 66 | async def _nodes_found(self, responses): 67 | raise NotImplementedError 68 | 69 | 70 | class ValueSpiderCrawl(SpiderCrawl): 71 | def __init__(self, protocol, node, peers, ksize, alpha): 72 | SpiderCrawl.__init__(self, protocol, node, peers, ksize, alpha) 73 | # keep track of the single nearest node without value - per 74 | # section 2.3 so we can set the key there if found 75 | self.nearest_without_value = NodeHeap(self.node, 1) 76 | 77 | async def find(self): 78 | """ 79 | Find either the closest nodes or the value requested. 80 | """ 81 | return await self._find(self.protocol.call_find_value) 82 | 83 | async def _nodes_found(self, responses): 84 | """ 85 | Handle the result of an iteration in _find. 86 | """ 87 | toremove = [] 88 | found_values = [] 89 | for peerid, response in responses.items(): 90 | response = RPCFindResponse(response) 91 | if not response.happened(): 92 | toremove.append(peerid) 93 | elif response.has_value(): 94 | found_values.append(response.get_value()) 95 | else: 96 | peer = self.nearest.get_node(peerid) 97 | self.nearest_without_value.push(peer) 98 | self.nearest.push(response.get_node_list()) 99 | self.nearest.remove(toremove) 100 | 101 | if found_values: 102 | return await self._handle_found_values(found_values) 103 | if self.nearest.have_contacted_all(): 104 | # not found! 105 | return None 106 | return await self.find() 107 | 108 | async def _handle_found_values(self, values): 109 | """ 110 | We got some values! Exciting. But let's make sure 111 | they're all the same or freak out a little bit. Also, 112 | make sure we tell the nearest node that *didn't* have 113 | the value to store it. 114 | """ 115 | value_counts = Counter(values) 116 | if len(value_counts) != 1: 117 | log.warning( 118 | "Got multiple values for key %i: %s", self.node.long_id, str(values) 119 | ) 120 | value = value_counts.most_common(1)[0][0] 121 | 122 | peer = self.nearest_without_value.popleft() 123 | if peer: 124 | await self.protocol.call_store(peer, self.node.id, value) 125 | return value 126 | 127 | 128 | class NodeSpiderCrawl(SpiderCrawl): 129 | async def find(self): 130 | """ 131 | Find the closest nodes. 132 | """ 133 | return await self._find(self.protocol.call_find_node) 134 | 135 | async def _nodes_found(self, responses): 136 | """ 137 | Handle the result of an iteration in _find. 138 | """ 139 | toremove = [] 140 | for peerid, response in responses.items(): 141 | response = RPCFindResponse(response) 142 | if not response.happened(): 143 | toremove.append(peerid) 144 | else: 145 | self.nearest.push(response.get_node_list()) 146 | self.nearest.remove(toremove) 147 | 148 | if self.nearest.have_contacted_all(): 149 | return list(self.nearest) 150 | return await self.find() 151 | 152 | 153 | class RPCFindResponse: 154 | def __init__(self, response): 155 | """ 156 | A wrapper for the result of a RPC find. 157 | 158 | Args: 159 | response: This will be a tuple of (, ) 160 | where will be a list of tuples if not found or 161 | a dictionary of {'value': v} where v is the value desired 162 | """ 163 | self.response = response 164 | 165 | def happened(self): 166 | """ 167 | Did the other host actually respond? 168 | """ 169 | return self.response[0] 170 | 171 | def has_value(self): 172 | return isinstance(self.response[1], dict) 173 | 174 | def get_value(self): 175 | return self.response[1]["value"] 176 | 177 | def get_node_list(self): 178 | """ 179 | Get the node list in the response. If there's no value, this should 180 | be set. 181 | """ 182 | nodelist = self.response[1] or [] 183 | return [Node(*nodeple) for nodeple in nodelist] 184 | -------------------------------------------------------------------------------- /kademlia/routing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import heapq 3 | import operator 4 | import time 5 | from collections import OrderedDict 6 | from itertools import chain 7 | 8 | from kademlia.utils import bytes_to_bit_string, shared_prefix 9 | 10 | 11 | class KBucket: 12 | def __init__(self, rangeLower, rangeUpper, ksize, replacementNodeFactor=5): 13 | self.range = (rangeLower, rangeUpper) 14 | self.nodes = OrderedDict() 15 | self.replacement_nodes = OrderedDict() 16 | self.touch_last_updated() 17 | self.ksize = ksize 18 | self.max_replacement_nodes = self.ksize * replacementNodeFactor 19 | 20 | def touch_last_updated(self): 21 | self.last_updated = time.monotonic() 22 | 23 | def get_nodes(self): 24 | return list(self.nodes.values()) 25 | 26 | def split(self): 27 | midpoint = (self.range[0] + self.range[1]) // 2 28 | one = KBucket(self.range[0], midpoint, self.ksize) 29 | two = KBucket(midpoint + 1, self.range[1], self.ksize) 30 | nodes = chain(self.nodes.values(), self.replacement_nodes.values()) 31 | for node in nodes: 32 | bucket = one if node.long_id <= midpoint else two 33 | bucket.add_node(node) 34 | 35 | return (one, two) 36 | 37 | def remove_node(self, node): 38 | if node.id in self.replacement_nodes: 39 | del self.replacement_nodes[node.id] 40 | 41 | if node.id in self.nodes: 42 | del self.nodes[node.id] 43 | 44 | if self.replacement_nodes: 45 | newnode_id, newnode = self.replacement_nodes.popitem() 46 | self.nodes[newnode_id] = newnode 47 | 48 | def has_in_range(self, node): 49 | return self.range[0] <= node.long_id <= self.range[1] 50 | 51 | def is_new_node(self, node): 52 | return node.id not in self.nodes 53 | 54 | def add_node(self, node): 55 | """ 56 | Add a C{Node} to the C{KBucket}. Return True if successful, 57 | False if the bucket is full. 58 | 59 | If the bucket is full, keep track of node in a replacement list, 60 | per section 4.1 of the paper. 61 | """ 62 | if node.id in self.nodes: 63 | del self.nodes[node.id] 64 | self.nodes[node.id] = node 65 | elif len(self) < self.ksize: 66 | self.nodes[node.id] = node 67 | else: 68 | if node.id in self.replacement_nodes: 69 | del self.replacement_nodes[node.id] 70 | self.replacement_nodes[node.id] = node 71 | while len(self.replacement_nodes) > self.max_replacement_nodes: 72 | self.replacement_nodes.popitem(last=False) 73 | return False 74 | return True 75 | 76 | def depth(self): 77 | vals = self.nodes.values() 78 | sprefix = shared_prefix([bytes_to_bit_string(n.id) for n in vals]) 79 | return len(sprefix) 80 | 81 | def head(self): 82 | return list(self.nodes.values())[0] 83 | 84 | def __getitem__(self, node_id): 85 | return self.nodes.get(node_id, None) 86 | 87 | def __len__(self): 88 | return len(self.nodes) 89 | 90 | 91 | class TableTraverser: 92 | def __init__(self, table, startNode): 93 | index = table.get_bucket_for(startNode) 94 | table.buckets[index].touch_last_updated() 95 | self.current_nodes = table.buckets[index].get_nodes() 96 | self.left_buckets = table.buckets[:index] 97 | self.right_buckets = table.buckets[(index + 1) :] 98 | self.left = True 99 | 100 | def __iter__(self): 101 | return self 102 | 103 | def __next__(self): 104 | """ 105 | Pop an item from the left subtree, then right, then left, etc. 106 | """ 107 | if self.current_nodes: 108 | return self.current_nodes.pop() 109 | 110 | if self.left and self.left_buckets: 111 | self.current_nodes = self.left_buckets.pop().get_nodes() 112 | self.left = False 113 | return next(self) 114 | 115 | if self.right_buckets: 116 | self.current_nodes = self.right_buckets.pop(0).get_nodes() 117 | self.left = True 118 | return next(self) 119 | 120 | raise StopIteration 121 | 122 | 123 | class RoutingTable: 124 | def __init__(self, protocol, ksize, node): 125 | """ 126 | @param node: The node that represents this server. It won't 127 | be added to the routing table, but will be needed later to 128 | determine which buckets to split or not. 129 | """ 130 | self.node = node 131 | self.protocol = protocol 132 | self.ksize = ksize 133 | self.flush() 134 | 135 | def flush(self): 136 | self.buckets = [KBucket(0, 2**160, self.ksize)] 137 | 138 | def split_bucket(self, index): 139 | one, two = self.buckets[index].split() 140 | self.buckets[index] = one 141 | self.buckets.insert(index + 1, two) 142 | 143 | def lonely_buckets(self): 144 | """ 145 | Get all of the buckets that haven't been updated in over 146 | an hour. 147 | """ 148 | hrago = time.monotonic() - 3600 149 | return [b for b in self.buckets if b.last_updated < hrago] 150 | 151 | def remove_contact(self, node): 152 | index = self.get_bucket_for(node) 153 | self.buckets[index].remove_node(node) 154 | 155 | def is_new_node(self, node): 156 | index = self.get_bucket_for(node) 157 | return self.buckets[index].is_new_node(node) 158 | 159 | def add_contact(self, node): 160 | index = self.get_bucket_for(node) 161 | bucket = self.buckets[index] 162 | 163 | # this will succeed unless the bucket is full 164 | if bucket.add_node(node): 165 | return 166 | 167 | # Per section 4.2 of paper, split if the bucket has the node 168 | # in its range or if the depth is not congruent to 0 mod 5 169 | if bucket.has_in_range(self.node) or bucket.depth() % 5 != 0: 170 | self.split_bucket(index) 171 | self.add_contact(node) 172 | else: 173 | asyncio.ensure_future(self.protocol.call_ping(bucket.head())) 174 | 175 | def get_bucket_for(self, node): 176 | """ 177 | Get the index of the bucket that the given node would fall into. 178 | """ 179 | for index, bucket in enumerate(self.buckets): 180 | if node.long_id < bucket.range[1]: 181 | return index 182 | # we should never be here, but make linter happy 183 | return None 184 | 185 | def find_neighbors(self, node, k=None, exclude=None): 186 | k = k or self.ksize 187 | nodes = [] 188 | for neighbor in TableTraverser(self, node): 189 | notexcluded = exclude is None or not neighbor.same_home_as(exclude) 190 | if neighbor.id != node.id and notexcluded: 191 | heapq.heappush(nodes, (node.distance_to(neighbor), neighbor)) 192 | if len(nodes) == k: 193 | break 194 | 195 | return list(map(operator.itemgetter(1), heapq.nsmallest(k, nodes))) 196 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Kademlia.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Kademlia.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Kademlia" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Kademlia" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Kademlia documentation build configuration file, created by 3 | # sphinx-quickstart on Mon Jan 5 09:42:46 2015. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | sys.path.insert(0, os.path.abspath("..")) 18 | import kademlia 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.todo", 36 | "sphinx.ext.viewcode", 37 | "sphinx.ext.napoleon", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = ".rst" 45 | 46 | # The encoding of source files. 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = "index" 51 | 52 | # General information about the project. 53 | project = "Kademlia" 54 | copyright = "2018, Brian Muller" 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = kademlia.__version__ 62 | # The full version, including alpha/beta/rc tags. 63 | release = version 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | # today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | # today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ["_build"] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | # default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | # add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | # add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | # show_authors = False 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = "sphinx" 96 | 97 | # A list of ignored prefixes for module index sorting. 98 | # modindex_common_prefix = [] 99 | 100 | # If true, keep warnings as "system message" paragraphs in the built documents. 101 | # keep_warnings = False 102 | 103 | 104 | # -- Options for HTML output ---------------------------------------------- 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | # on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 109 | # if on_rtd: 110 | # html_theme = "default" 111 | # else: 112 | html_theme = "sphinx_rtd_theme" 113 | 114 | html_theme_options = {} 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | # html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | # html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | # html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | # html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = [] 136 | 137 | # Add any extra paths that contain custom files (such as robots.txt or 138 | # .htaccess) here, relative to this directory. These files are copied 139 | # directly to the root of the documentation. 140 | # html_extra_path = [] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | # html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | # html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | # html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | # html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | # html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | # html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | # html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | # html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | # html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | # html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | # html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | # html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = "Kademliadoc" 185 | 186 | 187 | # -- Options for LaTeX output --------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | # Additional stuff for the LaTeX preamble. 195 | #'preamble': '', 196 | } 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, 200 | # author, documentclass [howto, manual, or own class]). 201 | latex_documents = [ 202 | ("index", "Kademlia.tex", "Kademlia Documentation", "Brian Muller", "manual"), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | # latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | # latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | # latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | # latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | # latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | # latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [("index", "kademlia", "Kademlia Documentation", ["Brian Muller"], 1)] 231 | 232 | # If true, show URL addresses after external links. 233 | # man_show_urls = False 234 | 235 | 236 | # -- Options for Texinfo output ------------------------------------------- 237 | 238 | # Grouping the document tree into Texinfo files. List of tuples 239 | # (source start file, target name, title, author, 240 | # dir menu entry, description, category) 241 | texinfo_documents = [ 242 | ( 243 | "index", 244 | "Kademlia", 245 | "Kademlia Documentation", 246 | "Brian Muller", 247 | "Kademlia", 248 | "One line description of project.", 249 | "Miscellaneous", 250 | ), 251 | ] 252 | 253 | # Documents to append as an appendix to all manuals. 254 | # texinfo_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | # texinfo_domain_indices = True 258 | 259 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 260 | # texinfo_show_urls = 'footnote' 261 | 262 | # If true, do not generate a @detailmenu in the "Top" node's menu. 263 | # texinfo_no_detailmenu = False 264 | 265 | autoclass_content = "both" 266 | -------------------------------------------------------------------------------- /kademlia/network.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package for interacting on the network at a high level. 3 | """ 4 | 5 | import asyncio 6 | import logging 7 | import pickle 8 | import random 9 | 10 | from kademlia.crawling import NodeSpiderCrawl, ValueSpiderCrawl 11 | from kademlia.node import Node 12 | from kademlia.protocol import KademliaProtocol 13 | from kademlia.storage import ForgetfulStorage 14 | from kademlia.utils import digest 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class Server: 20 | """ 21 | High level view of a node instance. This is the object that should be 22 | created to start listening as an active node on the network. 23 | """ 24 | 25 | protocol_class = KademliaProtocol 26 | 27 | def __init__(self, ksize=20, alpha=3, node_id=None, storage=None): 28 | """ 29 | Create a server instance. This will start listening on the given port. 30 | 31 | Args: 32 | ksize (int): The k parameter from the paper 33 | alpha (int): The alpha parameter from the paper 34 | node_id: The id for this node on the network. 35 | storage: An instance that implements the interface 36 | :class:`~kademlia.storage.IStorage` 37 | """ 38 | self.ksize = ksize 39 | self.alpha = alpha 40 | self.storage = storage or ForgetfulStorage() 41 | self.node = Node(node_id or digest(random.getrandbits(255))) 42 | self.transport = None 43 | self.protocol = None 44 | self.refresh_loop = None 45 | self.save_state_loop = None 46 | 47 | def stop(self): 48 | if self.transport is not None: 49 | self.transport.close() 50 | 51 | if self.refresh_loop: 52 | self.refresh_loop.cancel() 53 | 54 | if self.save_state_loop: 55 | self.save_state_loop.cancel() 56 | 57 | def _create_protocol(self): 58 | return self.protocol_class(self.node, self.storage, self.ksize) 59 | 60 | async def listen(self, port, interface="0.0.0.0"): 61 | """ 62 | Start listening on the given port. 63 | 64 | Provide interface="::" to accept ipv6 address 65 | """ 66 | loop = asyncio.get_event_loop() 67 | listen = loop.create_datagram_endpoint( 68 | self._create_protocol, local_addr=(interface, port) 69 | ) 70 | log.info("Node %i listening on %s:%i", self.node.long_id, interface, port) 71 | self.transport, self.protocol = await listen 72 | # finally, schedule refreshing table 73 | self.refresh_table() 74 | 75 | def refresh_table(self, interval=3600): 76 | log.debug("Refreshing routing table") 77 | asyncio.ensure_future(self._refresh_table()) 78 | loop = asyncio.get_event_loop() 79 | self.refresh_loop = loop.call_later(interval, self.refresh_table) 80 | 81 | async def _refresh_table(self): 82 | """ 83 | Refresh buckets that haven't had any lookups in the last hour 84 | (per section 2.3 of the paper). 85 | """ 86 | results = [] 87 | for node_id in self.protocol.get_refresh_ids(): 88 | node = Node(node_id) 89 | nearest = self.protocol.router.find_neighbors(node, self.alpha) 90 | spider = NodeSpiderCrawl( 91 | self.protocol, node, nearest, self.ksize, self.alpha 92 | ) 93 | results.append(spider.find()) 94 | 95 | # do our crawling 96 | await asyncio.gather(*results) 97 | 98 | # now republish keys older than one hour 99 | for dkey, value in self.storage.iter_older_than(3600): 100 | await self.set_digest(dkey, value) 101 | 102 | def bootstrappable_neighbors(self): 103 | """ 104 | Get a :class:`list` of (ip, port) :class:`tuple` pairs suitable for 105 | use as an argument to the bootstrap method. 106 | 107 | The server should have been bootstrapped 108 | already - this is just a utility for getting some neighbors and then 109 | storing them if this server is going down for a while. When it comes 110 | back up, the list of nodes can be used to bootstrap. 111 | """ 112 | neighbors = self.protocol.router.find_neighbors(self.node) 113 | return [tuple(n)[-2:] for n in neighbors] 114 | 115 | async def bootstrap(self, addrs): 116 | """ 117 | Bootstrap the server by connecting to other known nodes in the network. 118 | 119 | Args: 120 | addrs: A `list` of (ip, port) `tuple` pairs. Note that only IP 121 | addresses are acceptable - hostnames will cause an error. 122 | """ 123 | log.debug("Attempting to bootstrap node with %i initial contacts", len(addrs)) 124 | cos = list(map(self.bootstrap_node, addrs)) 125 | gathered = await asyncio.gather(*cos) 126 | nodes = [node for node in gathered if node is not None] 127 | spider = NodeSpiderCrawl( 128 | self.protocol, self.node, nodes, self.ksize, self.alpha 129 | ) 130 | return await spider.find() 131 | 132 | async def bootstrap_node(self, addr): 133 | result = await self.protocol.ping(addr, self.node.id) 134 | return Node(result[1], addr[0], addr[1]) if result[0] else None 135 | 136 | async def get(self, key): 137 | """ 138 | Get a key if the network has it. 139 | 140 | Returns: 141 | :class:`None` if not found, the value otherwise. 142 | """ 143 | log.info("Looking up key %s", key) 144 | dkey = digest(key) 145 | # if this node has it, return it 146 | if self.storage.get(dkey) is not None: 147 | return self.storage.get(dkey) 148 | node = Node(dkey) 149 | nearest = self.protocol.router.find_neighbors(node) 150 | if not nearest: 151 | log.warning("There are no known neighbors to get key %s", key) 152 | return None 153 | spider = ValueSpiderCrawl(self.protocol, node, nearest, self.ksize, self.alpha) 154 | return await spider.find() 155 | 156 | async def set(self, key, value): 157 | """ 158 | Set the given string key to the given value in the network. 159 | """ 160 | if not check_dht_value_type(value): 161 | raise TypeError("Value must be of type int, float, bool, str, or bytes") 162 | log.info("setting '%s' = '%s' on network", key, value) 163 | dkey = digest(key) 164 | return await self.set_digest(dkey, value) 165 | 166 | async def set_digest(self, dkey, value): 167 | """ 168 | Set the given SHA1 digest key (bytes) to the given value in the 169 | network. 170 | """ 171 | node = Node(dkey) 172 | 173 | nearest = self.protocol.router.find_neighbors(node) 174 | if not nearest: 175 | log.warning("There are no known neighbors to set key %s", dkey.hex()) 176 | return False 177 | 178 | spider = NodeSpiderCrawl(self.protocol, node, nearest, self.ksize, self.alpha) 179 | nodes = await spider.find() 180 | log.info("setting '%s' on %s", dkey.hex(), list(map(str, nodes))) 181 | 182 | # if this node is close too, then store here as well 183 | biggest = max([n.distance_to(node) for n in nodes]) 184 | if self.node.distance_to(node) < biggest: 185 | self.storage[dkey] = value 186 | results = [self.protocol.call_store(n, dkey, value) for n in nodes] 187 | # return true only if at least one store call succeeded 188 | return any(await asyncio.gather(*results)) 189 | 190 | def save_state(self, fname): 191 | """ 192 | Save the state of this node (the alpha/ksize/id/immediate neighbors) 193 | to a cache file with the given fname. 194 | """ 195 | log.info("Saving state to %s", fname) 196 | data = { 197 | "ksize": self.ksize, 198 | "alpha": self.alpha, 199 | "id": self.node.id, 200 | "neighbors": self.bootstrappable_neighbors(), 201 | } 202 | if not data["neighbors"]: 203 | log.warning("No known neighbors, so not writing to cache.") 204 | return 205 | with open(fname, "wb") as file: 206 | pickle.dump(data, file) 207 | 208 | @classmethod 209 | async def load_state(cls, fname, port, interface="0.0.0.0"): 210 | """ 211 | Load the state of this node (the alpha/ksize/id/immediate neighbors) 212 | from a cache file with the given fname and then bootstrap the node 213 | (using the given port/interface to start listening/bootstrapping). 214 | """ 215 | log.info("Loading state from %s", fname) 216 | with open(fname, "rb") as file: 217 | data = pickle.load(file) 218 | svr = cls(data["ksize"], data["alpha"], data["id"]) 219 | await svr.listen(port, interface) 220 | if data["neighbors"]: 221 | await svr.bootstrap(data["neighbors"]) 222 | return svr 223 | 224 | def save_state_regularly(self, fname, frequency=600): 225 | """ 226 | Save the state of node with a given regularity to the given 227 | filename. 228 | 229 | Args: 230 | fname: File name to save retularly to 231 | frequency: Frequency in seconds that the state should be saved. 232 | By default, 10 minutes. 233 | """ 234 | self.save_state(fname) 235 | loop = asyncio.get_event_loop() 236 | self.save_state_loop = loop.call_later( 237 | frequency, self.save_state_regularly, fname, frequency 238 | ) 239 | 240 | 241 | def check_dht_value_type(value): 242 | """ 243 | Checks to see if the type of the value is a valid type for 244 | placing in the dht. 245 | """ 246 | typeset = [int, float, bool, str, bytes] 247 | return type(value) in typeset 248 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.9" 4 | resolution-markers = [ 5 | "python_full_version >= '3.11'", 6 | "python_full_version == '3.10.*'", 7 | "python_full_version < '3.10'", 8 | ] 9 | 10 | [[package]] 11 | name = "alabaster" 12 | version = "0.7.16" 13 | source = { registry = "https://pypi.org/simple" } 14 | resolution-markers = [ 15 | "python_full_version < '3.10'", 16 | ] 17 | sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } 18 | wheels = [ 19 | { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, 20 | ] 21 | 22 | [[package]] 23 | name = "alabaster" 24 | version = "1.0.0" 25 | source = { registry = "https://pypi.org/simple" } 26 | resolution-markers = [ 27 | "python_full_version >= '3.11'", 28 | "python_full_version == '3.10.*'", 29 | ] 30 | sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, 33 | ] 34 | 35 | [[package]] 36 | name = "babel" 37 | version = "2.17.0" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, 42 | ] 43 | 44 | [[package]] 45 | name = "certifi" 46 | version = "2025.1.31" 47 | source = { registry = "https://pypi.org/simple" } 48 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 49 | wheels = [ 50 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 51 | ] 52 | 53 | [[package]] 54 | name = "charset-normalizer" 55 | version = "3.4.1" 56 | source = { registry = "https://pypi.org/simple" } 57 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 58 | wheels = [ 59 | { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, 60 | { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, 61 | { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, 62 | { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, 63 | { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, 64 | { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, 65 | { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, 66 | { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, 67 | { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, 68 | { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, 69 | { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, 70 | { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, 71 | { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, 72 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 73 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 74 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 75 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 76 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 77 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 78 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 79 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 80 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 81 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 82 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 83 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 84 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 85 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 86 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 87 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 88 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 89 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 90 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 91 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 92 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 93 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 94 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 95 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 96 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 97 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 98 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 99 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 100 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 101 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 102 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 103 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 104 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 105 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 106 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 107 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 108 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 109 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 110 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 111 | { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, 112 | { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, 113 | { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, 114 | { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, 115 | { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, 116 | { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, 117 | { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, 118 | { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, 119 | { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, 120 | { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, 121 | { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, 122 | { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, 123 | { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, 124 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 125 | ] 126 | 127 | [[package]] 128 | name = "colorama" 129 | version = "0.4.6" 130 | source = { registry = "https://pypi.org/simple" } 131 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 134 | ] 135 | 136 | [[package]] 137 | name = "coverage" 138 | version = "7.8.0" 139 | source = { registry = "https://pypi.org/simple" } 140 | sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } 141 | wheels = [ 142 | { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, 143 | { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, 144 | { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, 145 | { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, 146 | { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, 147 | { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, 148 | { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, 149 | { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, 150 | { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, 151 | { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, 152 | { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, 153 | { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, 154 | { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, 155 | { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, 156 | { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, 157 | { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, 158 | { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, 159 | { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, 160 | { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, 161 | { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, 162 | { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, 163 | { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, 164 | { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, 165 | { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, 166 | { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, 167 | { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, 168 | { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, 169 | { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, 170 | { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, 171 | { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, 172 | { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, 173 | { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, 174 | { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, 175 | { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, 176 | { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, 177 | { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, 178 | { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, 179 | { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, 180 | { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, 181 | { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, 182 | { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, 183 | { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, 184 | { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, 185 | { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, 186 | { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, 187 | { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, 188 | { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, 189 | { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, 190 | { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, 191 | { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, 192 | { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377 }, 193 | { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803 }, 194 | { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561 }, 195 | { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488 }, 196 | { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589 }, 197 | { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366 }, 198 | { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591 }, 199 | { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572 }, 200 | { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966 }, 201 | { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852 }, 202 | { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, 203 | { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, 204 | ] 205 | 206 | [package.optional-dependencies] 207 | toml = [ 208 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 209 | ] 210 | 211 | [[package]] 212 | name = "docutils" 213 | version = "0.21.2" 214 | source = { registry = "https://pypi.org/simple" } 215 | sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } 216 | wheels = [ 217 | { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, 218 | ] 219 | 220 | [[package]] 221 | name = "exceptiongroup" 222 | version = "1.2.2" 223 | source = { registry = "https://pypi.org/simple" } 224 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 225 | wheels = [ 226 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 227 | ] 228 | 229 | [[package]] 230 | name = "idna" 231 | version = "3.10" 232 | source = { registry = "https://pypi.org/simple" } 233 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 234 | wheels = [ 235 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 236 | ] 237 | 238 | [[package]] 239 | name = "imagesize" 240 | version = "1.4.1" 241 | source = { registry = "https://pypi.org/simple" } 242 | sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } 243 | wheels = [ 244 | { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, 245 | ] 246 | 247 | [[package]] 248 | name = "importlib-metadata" 249 | version = "8.6.1" 250 | source = { registry = "https://pypi.org/simple" } 251 | dependencies = [ 252 | { name = "zipp", marker = "python_full_version < '3.10'" }, 253 | ] 254 | sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, 257 | ] 258 | 259 | [[package]] 260 | name = "iniconfig" 261 | version = "2.1.0" 262 | source = { registry = "https://pypi.org/simple" } 263 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, 266 | ] 267 | 268 | [[package]] 269 | name = "jinja2" 270 | version = "3.1.6" 271 | source = { registry = "https://pypi.org/simple" } 272 | dependencies = [ 273 | { name = "markupsafe" }, 274 | ] 275 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } 276 | wheels = [ 277 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, 278 | ] 279 | 280 | [[package]] 281 | name = "kademlia" 282 | source = { editable = "." } 283 | dependencies = [ 284 | { name = "rpcudp" }, 285 | ] 286 | 287 | [package.dev-dependencies] 288 | dev = [ 289 | { name = "pytest" }, 290 | { name = "pytest-asyncio" }, 291 | { name = "pytest-cov" }, 292 | { name = "ruff" }, 293 | ] 294 | docs = [ 295 | { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 296 | { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, 297 | { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 298 | { name = "sphinx-rtd-theme" }, 299 | ] 300 | 301 | [package.metadata] 302 | requires-dist = [{ name = "rpcudp", specifier = ">=5.0.1" }] 303 | 304 | [package.metadata.requires-dev] 305 | dev = [ 306 | { name = "pytest", specifier = ">=8.3.5" }, 307 | { name = "pytest-asyncio", specifier = ">=0.26.0" }, 308 | { name = "pytest-cov", specifier = ">=6.0.0" }, 309 | { name = "ruff", specifier = ">=0.11.2" }, 310 | ] 311 | docs = [ 312 | { name = "sphinx", specifier = ">=7.4.7" }, 313 | { name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, 314 | ] 315 | 316 | [[package]] 317 | name = "markupsafe" 318 | version = "3.0.2" 319 | source = { registry = "https://pypi.org/simple" } 320 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, 323 | { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, 324 | { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, 325 | { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, 326 | { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, 327 | { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, 328 | { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, 329 | { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, 330 | { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, 331 | { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, 332 | { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, 333 | { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, 334 | { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, 335 | { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, 336 | { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, 337 | { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, 338 | { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, 339 | { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, 340 | { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, 341 | { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, 342 | { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, 343 | { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, 344 | { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, 345 | { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, 346 | { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, 347 | { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, 348 | { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, 349 | { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, 350 | { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, 351 | { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, 352 | { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, 353 | { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, 354 | { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, 355 | { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, 356 | { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, 357 | { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, 358 | { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, 359 | { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, 360 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, 361 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, 362 | { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, 363 | { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, 364 | { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, 365 | { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, 366 | { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, 367 | { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, 368 | { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, 369 | { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, 370 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, 371 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, 372 | { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, 373 | { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, 374 | { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, 375 | { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, 376 | { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, 377 | { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, 378 | { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, 379 | { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, 380 | { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, 381 | { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, 382 | ] 383 | 384 | [[package]] 385 | name = "packaging" 386 | version = "24.2" 387 | source = { registry = "https://pypi.org/simple" } 388 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 389 | wheels = [ 390 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 391 | ] 392 | 393 | [[package]] 394 | name = "pluggy" 395 | version = "1.5.0" 396 | source = { registry = "https://pypi.org/simple" } 397 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 398 | wheels = [ 399 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 400 | ] 401 | 402 | [[package]] 403 | name = "pygments" 404 | version = "2.19.1" 405 | source = { registry = "https://pypi.org/simple" } 406 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 407 | wheels = [ 408 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 409 | ] 410 | 411 | [[package]] 412 | name = "pytest" 413 | version = "8.3.5" 414 | source = { registry = "https://pypi.org/simple" } 415 | dependencies = [ 416 | { name = "colorama", marker = "sys_platform == 'win32'" }, 417 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 418 | { name = "iniconfig" }, 419 | { name = "packaging" }, 420 | { name = "pluggy" }, 421 | { name = "tomli", marker = "python_full_version < '3.11'" }, 422 | ] 423 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 424 | wheels = [ 425 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 426 | ] 427 | 428 | [[package]] 429 | name = "pytest-asyncio" 430 | version = "0.26.0" 431 | source = { registry = "https://pypi.org/simple" } 432 | dependencies = [ 433 | { name = "pytest" }, 434 | { name = "typing-extensions", marker = "python_full_version < '3.10'" }, 435 | ] 436 | sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 } 437 | wheels = [ 438 | { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 }, 439 | ] 440 | 441 | [[package]] 442 | name = "pytest-cov" 443 | version = "6.0.0" 444 | source = { registry = "https://pypi.org/simple" } 445 | dependencies = [ 446 | { name = "coverage", extra = ["toml"] }, 447 | { name = "pytest" }, 448 | ] 449 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, 452 | ] 453 | 454 | [[package]] 455 | name = "requests" 456 | version = "2.32.3" 457 | source = { registry = "https://pypi.org/simple" } 458 | dependencies = [ 459 | { name = "certifi" }, 460 | { name = "charset-normalizer" }, 461 | { name = "idna" }, 462 | { name = "urllib3" }, 463 | ] 464 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 465 | wheels = [ 466 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 467 | ] 468 | 469 | [[package]] 470 | name = "roman-numerals-py" 471 | version = "3.1.0" 472 | source = { registry = "https://pypi.org/simple" } 473 | sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } 474 | wheels = [ 475 | { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, 476 | ] 477 | 478 | [[package]] 479 | name = "rpcudp" 480 | version = "5.0.1" 481 | source = { registry = "https://pypi.org/simple" } 482 | dependencies = [ 483 | { name = "u-msgpack-python" }, 484 | ] 485 | sdist = { url = "https://files.pythonhosted.org/packages/39/5b/99ee4dd6080d857f029ad209860d461305f5fba9fef2316548a1d131e4c2/rpcudp-5.0.1.tar.gz", hash = "sha256:b6793b9b3e84e9c8510fa78e259cc3204c1b03fd2bb4fbf0f457cd391933bb78", size = 8777 } 486 | wheels = [ 487 | { url = "https://files.pythonhosted.org/packages/05/8b/33f35f2a730f848b5e5ac24dcb50c97387b98756e0f199ee8da30ef19fe7/rpcudp-5.0.1-py3-none-any.whl", hash = "sha256:8bf7cf1caed687acbbf2a37b67a92b02d109483a2da28fa13cca93b9ee873b9a", size = 5594 }, 488 | ] 489 | 490 | [[package]] 491 | name = "ruff" 492 | version = "0.11.2" 493 | source = { registry = "https://pypi.org/simple" } 494 | sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } 495 | wheels = [ 496 | { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, 497 | { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, 498 | { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, 499 | { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, 500 | { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, 501 | { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, 502 | { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, 503 | { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, 504 | { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, 505 | { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, 506 | { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, 507 | { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, 508 | { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, 509 | { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, 510 | { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, 511 | { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, 512 | { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, 513 | ] 514 | 515 | [[package]] 516 | name = "snowballstemmer" 517 | version = "2.2.0" 518 | source = { registry = "https://pypi.org/simple" } 519 | sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } 520 | wheels = [ 521 | { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, 522 | ] 523 | 524 | [[package]] 525 | name = "sphinx" 526 | version = "7.4.7" 527 | source = { registry = "https://pypi.org/simple" } 528 | resolution-markers = [ 529 | "python_full_version < '3.10'", 530 | ] 531 | dependencies = [ 532 | { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 533 | { name = "babel", marker = "python_full_version < '3.10'" }, 534 | { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, 535 | { name = "docutils", marker = "python_full_version < '3.10'" }, 536 | { name = "imagesize", marker = "python_full_version < '3.10'" }, 537 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 538 | { name = "jinja2", marker = "python_full_version < '3.10'" }, 539 | { name = "packaging", marker = "python_full_version < '3.10'" }, 540 | { name = "pygments", marker = "python_full_version < '3.10'" }, 541 | { name = "requests", marker = "python_full_version < '3.10'" }, 542 | { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, 543 | { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, 544 | { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, 545 | { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, 546 | { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, 547 | { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, 548 | { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, 549 | { name = "tomli", marker = "python_full_version < '3.10'" }, 550 | ] 551 | sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 } 552 | wheels = [ 553 | { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 }, 554 | ] 555 | 556 | [[package]] 557 | name = "sphinx" 558 | version = "8.1.3" 559 | source = { registry = "https://pypi.org/simple" } 560 | resolution-markers = [ 561 | "python_full_version == '3.10.*'", 562 | ] 563 | dependencies = [ 564 | { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, 565 | { name = "babel", marker = "python_full_version == '3.10.*'" }, 566 | { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, 567 | { name = "docutils", marker = "python_full_version == '3.10.*'" }, 568 | { name = "imagesize", marker = "python_full_version == '3.10.*'" }, 569 | { name = "jinja2", marker = "python_full_version == '3.10.*'" }, 570 | { name = "packaging", marker = "python_full_version == '3.10.*'" }, 571 | { name = "pygments", marker = "python_full_version == '3.10.*'" }, 572 | { name = "requests", marker = "python_full_version == '3.10.*'" }, 573 | { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, 574 | { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, 575 | { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, 576 | { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, 577 | { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, 578 | { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, 579 | { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, 580 | { name = "tomli", marker = "python_full_version == '3.10.*'" }, 581 | ] 582 | sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } 583 | wheels = [ 584 | { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, 585 | ] 586 | 587 | [[package]] 588 | name = "sphinx" 589 | version = "8.2.3" 590 | source = { registry = "https://pypi.org/simple" } 591 | resolution-markers = [ 592 | "python_full_version >= '3.11'", 593 | ] 594 | dependencies = [ 595 | { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 596 | { name = "babel", marker = "python_full_version >= '3.11'" }, 597 | { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, 598 | { name = "docutils", marker = "python_full_version >= '3.11'" }, 599 | { name = "imagesize", marker = "python_full_version >= '3.11'" }, 600 | { name = "jinja2", marker = "python_full_version >= '3.11'" }, 601 | { name = "packaging", marker = "python_full_version >= '3.11'" }, 602 | { name = "pygments", marker = "python_full_version >= '3.11'" }, 603 | { name = "requests", marker = "python_full_version >= '3.11'" }, 604 | { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, 605 | { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, 606 | { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, 607 | { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, 608 | { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, 609 | { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, 610 | { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, 611 | { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, 612 | ] 613 | sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } 614 | wheels = [ 615 | { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741 }, 616 | ] 617 | 618 | [[package]] 619 | name = "sphinx-rtd-theme" 620 | version = "3.0.2" 621 | source = { registry = "https://pypi.org/simple" } 622 | dependencies = [ 623 | { name = "docutils" }, 624 | { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 625 | { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, 626 | { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 627 | { name = "sphinxcontrib-jquery" }, 628 | ] 629 | sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463 } 630 | wheels = [ 631 | { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561 }, 632 | ] 633 | 634 | [[package]] 635 | name = "sphinxcontrib-applehelp" 636 | version = "2.0.0" 637 | source = { registry = "https://pypi.org/simple" } 638 | sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } 639 | wheels = [ 640 | { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, 641 | ] 642 | 643 | [[package]] 644 | name = "sphinxcontrib-devhelp" 645 | version = "2.0.0" 646 | source = { registry = "https://pypi.org/simple" } 647 | sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } 648 | wheels = [ 649 | { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, 650 | ] 651 | 652 | [[package]] 653 | name = "sphinxcontrib-htmlhelp" 654 | version = "2.1.0" 655 | source = { registry = "https://pypi.org/simple" } 656 | sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } 657 | wheels = [ 658 | { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, 659 | ] 660 | 661 | [[package]] 662 | name = "sphinxcontrib-jquery" 663 | version = "4.1" 664 | source = { registry = "https://pypi.org/simple" } 665 | dependencies = [ 666 | { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, 667 | { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, 668 | { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 669 | ] 670 | sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331 } 671 | wheels = [ 672 | { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104 }, 673 | ] 674 | 675 | [[package]] 676 | name = "sphinxcontrib-jsmath" 677 | version = "1.0.1" 678 | source = { registry = "https://pypi.org/simple" } 679 | sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } 680 | wheels = [ 681 | { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, 682 | ] 683 | 684 | [[package]] 685 | name = "sphinxcontrib-qthelp" 686 | version = "2.0.0" 687 | source = { registry = "https://pypi.org/simple" } 688 | sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } 689 | wheels = [ 690 | { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, 691 | ] 692 | 693 | [[package]] 694 | name = "sphinxcontrib-serializinghtml" 695 | version = "2.0.0" 696 | source = { registry = "https://pypi.org/simple" } 697 | sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } 698 | wheels = [ 699 | { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, 700 | ] 701 | 702 | [[package]] 703 | name = "tomli" 704 | version = "2.2.1" 705 | source = { registry = "https://pypi.org/simple" } 706 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 707 | wheels = [ 708 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 709 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 710 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 711 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 712 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 713 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 714 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 715 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 716 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 717 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 718 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 719 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 720 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 721 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 722 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 723 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 724 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 725 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 726 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 727 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 728 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 729 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 730 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 731 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 732 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 733 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 734 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 735 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 736 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 737 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 738 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 739 | ] 740 | 741 | [[package]] 742 | name = "typing-extensions" 743 | version = "4.13.0" 744 | source = { registry = "https://pypi.org/simple" } 745 | sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } 746 | wheels = [ 747 | { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, 748 | ] 749 | 750 | [[package]] 751 | name = "u-msgpack-python" 752 | version = "2.8.0" 753 | source = { registry = "https://pypi.org/simple" } 754 | sdist = { url = "https://files.pythonhosted.org/packages/36/9d/a40411a475e7d4838994b7f6bcc6bfca9acc5b119ce3a7503608c4428b49/u-msgpack-python-2.8.0.tar.gz", hash = "sha256:b801a83d6ed75e6df41e44518b4f2a9c221dc2da4bcd5380e3a0feda520bc61a", size = 18167 } 755 | wheels = [ 756 | { url = "https://files.pythonhosted.org/packages/b1/5e/512aeb40fd819f4660d00f96f5c7371ee36fc8c6b605128c5ee59e0b28c6/u_msgpack_python-2.8.0-py2.py3-none-any.whl", hash = "sha256:1d853d33e78b72c4228a2025b4db28cda81214076e5b0422ed0ae1b1b2bb586a", size = 10590 }, 757 | ] 758 | 759 | [[package]] 760 | name = "urllib3" 761 | version = "2.3.0" 762 | source = { registry = "https://pypi.org/simple" } 763 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 764 | wheels = [ 765 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 766 | ] 767 | 768 | [[package]] 769 | name = "zipp" 770 | version = "3.21.0" 771 | source = { registry = "https://pypi.org/simple" } 772 | sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } 773 | wheels = [ 774 | { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, 775 | ] 776 | --------------------------------------------------------------------------------