├── slist ├── py.typed ├── pydantic_compat.py └── __init__.py ├── tests ├── __init__.py ├── run_tqdm.py ├── test_docs.py ├── test_mypy.py └── test_slist.py ├── requirements.txt ├── mypy.ini ├── .gitignore ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── docs.yml │ ├── dev.yml │ └── release.yml ├── docs ├── api │ └── slist.md ├── index.md └── contributing.md ├── LICENSE ├── mkdocs.yml ├── pyproject.toml ├── README.md ├── CONTRIBUTING.md └── poetry.lock /slist/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==7.2.0 2 | pytest-asyncio==0.20.2 3 | typing-extensions -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = slist/ 3 | disallow_any_generics = True 4 | check_untyped_defs = True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__/ 3 | .pytest_cache/ 4 | .DS_Store 5 | .idea 6 | .mypy_cache 7 | .ipynb_checkpoints 8 | venv 9 | db/ 10 | dump.rdb 11 | config.py 12 | .dmypy.json 13 | .env 14 | site -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * svector version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /tests/run_tqdm.py: -------------------------------------------------------------------------------- 1 | from slist import Slist 2 | 3 | 4 | async def test_par_map_async_max_parallel_tqdm(): 5 | async def func(x: int) -> int: 6 | # wait 2 s 7 | await asyncio.sleep(2) 8 | return x * 2 9 | 10 | result = await Slist([1, 2, 3]).par_map_async(func, max_par=1, tqdm=True) 11 | assert result == Slist([2, 4, 6]) 12 | 13 | 14 | if __name__ == "__main__": 15 | import asyncio 16 | 17 | asyncio.run(test_par_map_async_max_parallel_tqdm()) 18 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | def test_docs_chain_example(): 2 | from slist import Slist 3 | 4 | result = ( 5 | Slist([1, 2, 3]) 6 | .repeat_until_size_or_raise(20) 7 | .grouped(2) 8 | .map(lambda inner_list: inner_list[0] + inner_list[1] if inner_list.length == 2 else inner_list[0]) 9 | .flatten_option() 10 | .distinct_by(lambda x: x) 11 | .map(str) 12 | .reversed() 13 | .mk_string(sep=",") 14 | ) 15 | assert result == "5,4,3" 16 | -------------------------------------------------------------------------------- /docs/api/slist.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Slist Class 4 | 5 | ::: slist.Slist 6 | options: 7 | show_root_heading: true 8 | show_source: true 9 | heading_level: 2 10 | 11 | ## Helper Classes 12 | 13 | ### Group 14 | 15 | ::: slist.Group 16 | options: 17 | show_root_heading: true 18 | show_source: true 19 | heading_level: 3 20 | 21 | ### AverageStats 22 | 23 | ::: slist.AverageStats 24 | options: 25 | show_root_heading: true 26 | show_source: true 27 | heading_level: 3 -------------------------------------------------------------------------------- /tests/test_mypy.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from slist import Slist 6 | 7 | 8 | @pytest.mark.skip 9 | def test_flatten_list(): 10 | nested: Slist[List[int]] = Slist([[10]]) 11 | test: Slist[int] = nested.flatten_list() # ok 12 | print(test) 13 | nested_slist: Slist[Slist[int]] = Slist([Slist([10])]) 14 | test_slist: Slist[int] = nested_slist.flatten_list() # ok 15 | print(test_slist) 16 | # Should be error: Invalid self argument "Slist[int]" to attribute function "flatten_list" with type "Callable[[Sequence[Sequence[B]]], Slist[B]]" 17 | Slist([1]).flatten_list() # type: ignore 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021, James Chua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | paths: 8 | - 'docs/**' 9 | - 'mkdocs.yml' 10 | - '.github/workflows/docs.yml' 11 | - 'slist/**' 12 | permissions: 13 | contents: write 14 | pages: write 15 | id-token: write 16 | jobs: 17 | deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.x 24 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 25 | - uses: actions/cache@v3 26 | with: 27 | key: mkdocs-material-${{ env.cache_id }} 28 | path: .cache 29 | restore-keys: | 30 | mkdocs-material- 31 | - run: pip install poetry 32 | - run: poetry install -E doc 33 | - run: pip install mkdocs-material mkdocs mkdocstrings[python] 34 | - run: pip install -e . 35 | - name: Build and Deploy Documentation 36 | run: | 37 | git config --global user.name "github-actions" 38 | git config --global user.email "github-actions@github.com" 39 | poetry run mkdocs build --verbose 40 | poetry run mkdocs gh-deploy --force --verbose -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Slist Documentation 2 | 3 | Slist is a typesafe list implementation for Python that provides enhanced method chaining capabilities and additional utility functions. 4 | 5 | ## Features 6 | 7 | - 🔒 Type-safe: Full type hints and mypy support 8 | - ⛓️ Method chaining: Fluent interface for list operations 9 | - 🛠️ Rich functionality: Many utility methods for common operations 10 | - 🚀 Performance: Minimal overhead over Python's built-in list 11 | - 🔍 Clear API: Well-documented methods with intuitive names 12 | 13 | ## Installation 14 | 15 | ```bash 16 | pip install slist 17 | ``` 18 | 19 | Or with Poetry: 20 | 21 | ```bash 22 | poetry add slist 23 | ``` 24 | 25 | ## Quick Example 26 | 27 | ```python 28 | from slist import Slist 29 | 30 | # Create a list of numbers 31 | numbers = Slist([1, 2, 3, 4, 5]) 32 | 33 | # Chain operations 34 | result = numbers\ 35 | .filter(lambda x: x % 2 == 0)\ # Keep even numbers 36 | .map(lambda x: x * 2)\ # Double each number 37 | .reversed()\ # Reverse the order 38 | .add_one(10) # Add one more number 39 | 40 | print(result) # Slist([10, 8, 4]) 41 | ``` 42 | 43 | ## Why Slist? 44 | 45 | Slist enhances Python's built-in list with: 46 | 47 | 1. Method chaining for cleaner code 48 | 2. Type-safe operations 49 | 3. Additional utility methods 50 | 4. Functional programming patterns 51 | 5. Async operation support 52 | 53 | Check out the [API Reference](api/slist.md) for detailed documentation of all available methods. -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: dev workflow 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master, main ] 10 | pull_request: 11 | branches: [ master, main ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "test" 19 | test: 20 | # The type of runner that the job will run on 21 | strategy: 22 | matrix: 23 | python-versions: [3.8, 3.9, "3.10", "3.11", "3.12"] 24 | os: [ubuntu-latest, macos-latest, windows-latest] 25 | runs-on: ${{ matrix.os }} 26 | 27 | # Steps represent a sequence of tasks that will be executed as part of the job 28 | steps: 29 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-versions }} 34 | cache: 'pip' 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install pytest 40 | pip install tqdm 41 | pip install numpy 42 | 43 | - name: list files 44 | run: ls -l . 45 | -------------------------------------------------------------------------------- /slist/pydantic_compat.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence, Generic, TYPE_CHECKING, cast 2 | 3 | from slist import Slist, A 4 | 5 | if TYPE_CHECKING: 6 | SlistPydantic = Slist 7 | else: 8 | 9 | class SlistPydanticImpl(Generic[A]): 10 | """ 11 | To use: 12 | >>> from pydantic import BaseModel 13 | >>>class MyModel(BaseModel): 14 | >>> my_slist: SlistPydantic[int] # rather than Slist[int]. Otherwise pydantic turns it into a normal list 15 | >>>my_instance = MyModel(my_slist=Slist([1,2,3])) 16 | >>>assert my_instance.my_slist.first_option == 1 17 | """ 18 | 19 | @classmethod 20 | def __get_validators__(cls): 21 | yield cls.validate 22 | 23 | @classmethod 24 | def validate(cls, v, field): # field: ModelField 25 | subfield = field.sub_fields[0] # e.g. the int type in Slist[int] 26 | if not isinstance(v, Sequence): 27 | raise TypeError(f"Sequence required to instantiate a Slist, got {v} of type {type(v)}") 28 | validated_values = [] 29 | for idx, item in enumerate(v): 30 | valid_value, error = subfield.validate(item, {}, loc=str(idx)) 31 | if error is not None: 32 | raise ValueError(f"Error validating {item}, Error: {error}") 33 | 34 | validated_values.append(valid_value) 35 | return Slist(validated_values) 36 | 37 | # Pycharm doesn't check if TYPE_CHECKING so we do this hack 38 | SlistPydantic = cast(Slist, SlistPydanticImpl) 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Publish package on main branch if it's tagged with 'v*' 2 | 3 | name: release & publish workflow 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push events but only for the master branch 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "release" 18 | release: 19 | name: Create Release 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write # Needed for creating GitHub release 23 | id-token: write # Needed for PyPI trusted publishing 24 | 25 | strategy: 26 | matrix: 27 | python-versions: ["3.12"] # only one needed 28 | 29 | # Steps represent a sequence of tasks that will be executed as part of the job 30 | steps: 31 | - name: Get version from tag 32 | id: tag_name 33 | run: | 34 | echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 35 | shell: bash 36 | 37 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-versions }} 42 | 43 | - name: Install dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | pip install poetry 47 | 48 | - name: Build wheels and source tarball 49 | run: >- 50 | poetry build 51 | 52 | - name: show temporary files 53 | run: >- 54 | ls -l 55 | 56 | 57 | - name: publish to PyPI 58 | uses: pypa/gh-action-pypi-publish@release/v1 59 | with: 60 | skip_existing: true 61 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Slist Documentation 2 | site_description: Documentation for the Slist Python package - a typesafe list with more method chaining! 3 | site_url: https://thejaminator.github.io/slist/ 4 | repo_url: https://github.com/thejaminator/slist 5 | repo_name: thejaminator/slist 6 | 7 | theme: 8 | name: material 9 | features: 10 | - navigation.instant 11 | - navigation.tracking 12 | - navigation.sections 13 | - navigation.expand 14 | - navigation.top 15 | - search.highlight 16 | - search.share 17 | - toc.follow 18 | palette: 19 | - scheme: default 20 | primary: indigo 21 | accent: indigo 22 | toggle: 23 | icon: material/brightness-7 24 | name: Switch to dark mode 25 | - scheme: slate 26 | primary: indigo 27 | accent: indigo 28 | toggle: 29 | icon: material/brightness-4 30 | name: Switch to light mode 31 | 32 | plugins: 33 | - search 34 | - mkdocstrings: 35 | default_handler: python 36 | handlers: 37 | python: 38 | paths: [slist] 39 | options: 40 | show_source: true 41 | show_root_heading: true 42 | show_category_heading: true 43 | show_bases: true 44 | heading_level: 2 45 | members_order: source 46 | show_if_no_docstring: true 47 | separate_signature: true 48 | docstring_section_style: table 49 | show_root_toc_entry: true 50 | show_root_members_full_path: false 51 | docstring_options: 52 | ignore_init_summary: true 53 | # hide_parameters: true 54 | # hide_returns: true 55 | # hide_examples: true 56 | members_order: source 57 | show_signature_annotations: true 58 | signature_crossrefs: true 59 | filters: ["!^_"] 60 | docstring_style: google 61 | show_docstring_attributes: true 62 | show_docstring_examples: false 63 | show_docstring_returns: false 64 | show_docstring_parameters: false 65 | 66 | markdown_extensions: 67 | - pymdownx.highlight: 68 | anchor_linenums: true 69 | - pymdownx.inlinehilite 70 | - pymdownx.snippets 71 | - pymdownx.superfences 72 | - admonition 73 | - pymdownx.details 74 | - attr_list 75 | - md_in_html 76 | - toc: 77 | permalink: true 78 | toc_depth: 4 79 | baselevel: 1 80 | 81 | nav: 82 | - Home: index.md 83 | - Slist: api/slist.md 84 | - Contributing: contributing.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool] 2 | [tool.poetry] 3 | name = "slist" 4 | version = "0.3.17" 5 | homepage = "https://github.com/thejaminator/slist" 6 | description = "A typesafe list with more method chaining!" 7 | authors = ["James Chua "] 8 | readme = "README.md" 9 | license = "MIT" 10 | classifiers=[ 11 | 'Development Status :: 2 - Pre-Alpha', 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Natural Language :: English', 15 | 'Programming Language :: Python :: 3.7', 16 | 'Programming Language :: Python :: 3.8', 17 | 'Programming Language :: Python :: 3.9', 18 | ] 19 | packages = [ 20 | { include = "slist" }, 21 | { include = "tests", format = "sdist" }, 22 | ] 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.8" 26 | typing-extensions = "^4.0.0" 27 | 28 | [tool.poetry.dev-dependencies] 29 | black = { version = "^21.5b2", optional = true} 30 | mypy = {version = "^0.900", optional = true} 31 | pytest = { version = "^6.2.4", optional = true} 32 | pytest-cov = { version = "^2.12.0", optional = true} 33 | tox = { version = "^3.20.1", optional = true} 34 | virtualenv = { version = "^20.2.2", optional = true} 35 | pip = { version = "^20.3.1", optional = true} 36 | toml = {version = "^0.10.2", optional = true} 37 | tqdm = {version = "^4.61.0", optional = true} 38 | numpy = {version = "^1.20.3", optional = true} 39 | 40 | [tool.poetry.extras] 41 | test = [ 42 | "pytest", 43 | "black", 44 | "mypy", 45 | "pytest-cov" 46 | ] 47 | 48 | dev = ["tox", "virtualenv", "pip", "twine", "toml"] 49 | 50 | doc = [ 51 | "mkdocs", 52 | "mkdocs-include-markdown-plugin", 53 | "mkdocs-material", 54 | "mkdocstrings", 55 | "mkdocs-material-extension", 56 | "mkdocs-autorefs" 57 | ] 58 | 59 | [tool.ruff] 60 | line-length = 120 61 | 62 | 63 | [build-system] 64 | requires = ["poetry-core>=1.0.0", "twine"] 65 | build-backend = "poetry.core.masonry.api" 66 | 67 | 68 | [tool.pyright] 69 | exclude = ["venv"] 70 | 71 | strictListInference = true 72 | strictDictionaryInference = true 73 | strictSetInference = true 74 | reportFunctionMemberAccess = true 75 | reportUnknownParameterType = true 76 | reportIncompatibleMethodOverride = true 77 | reportIncompatibleVariableOverride = true 78 | reportInconsistentConstructorType = true 79 | reportOverlappingOverload = true 80 | reportConstantRedefinition = true 81 | reportImportCycles = true 82 | reportPropertyTypeMismatch = true 83 | reportMissingTypeArgument = true 84 | reportUnnecessaryCast = true 85 | reportUnnecessaryComparison = true 86 | reportUnnecessaryContains = true 87 | reportUnusedExpression = true 88 | reportMatchNotExhaustive = true 89 | reportShadowedImports = true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slist 2 | This is a drop in replacement for the built-in mutable python list 3 | 4 | But with more post-fixed methods for chaining in a typesafe manner!! 5 | 6 | Leverage the latest pyright features to spot errors during coding. 7 | 8 | All these methods return a new list. They do not mutate the original list. 9 | 10 | Not able to convince your colleagues to use immutable functional data structures? I understand. 11 | This library lets you still have the benefits of typesafe chaining methods while letting your colleagues have their mutable lists! 12 | 13 | 14 | [![pypi](https://img.shields.io/pypi/v/slist.svg)](https://pypi.org/project/slist) 15 | [![python](https://img.shields.io/pypi/pyversions/slist.svg)](https://pypi.org/project/slist) 16 | [![Build Status](https://github.com/thejaminator/slist/actions/workflows/dev.yml/badge.svg)](https://github.com/thejaminator/slist/actions/workflows/dev.yml) 17 | 18 | ``` 19 | pip install slist 20 | ``` 21 | 22 | 23 | * Documentation: 24 | 25 | 26 | ## Quick Start 27 | Easily spot errors when you call the wrong methods on your sequence with mypy. 28 | 29 | ```python 30 | from slist import Slist 31 | 32 | many_strings = Slist(["Lucy, Damion, Jon"]) # Slist[str] 33 | many_strings.sum() # Mypy errors with 'Invalid self argument'. You can't sum a sequence of strings! 34 | 35 | many_nums = Slist([1, 1.2]) 36 | assert many_nums.sum() == 2.2 # ok! 37 | 38 | class CannotSortMe: 39 | def __init__(self, value: int): 40 | self.value: int = value 41 | 42 | stuff = Slist([CannotSortMe(value=1), CannotSortMe(value=1)]) 43 | stuff.sort_by(lambda x: x) # Mypy errors with 'Cannot be "CannotSortMe"'. There isn't a way to sort by the class itself 44 | stuff.sort_by(lambda x: x.value) # ok! You can sort by the value 45 | 46 | Slist([{"i am a dict": "value"}]).distinct_by( 47 | lambda x: x 48 | ) # Mypy errors with 'Cannot be Dict[str, str]. You can't hash a dict itself 49 | ``` 50 | 51 | Slist provides methods to easily flatten and infer the types of your data. 52 | ```python 53 | from slist import Slist, List 54 | from typing import Optional 55 | 56 | test_optional: Slist[Optional[int]] = Slist([-1, 0, 1]).map( 57 | lambda x: x if x >= 0 else None 58 | ) 59 | # Mypy infers slist[int] correctly 60 | test_flattened: Slist[int] = test_optional.flatten_option() 61 | 62 | 63 | test_nested: Slist[List[str]] = Slist([["bob"], ["dylan", "chan"]]) 64 | # Mypy infers slist[str] correctly 65 | test_flattened_str: Slist[str] = test_nested.flatten_list() 66 | ``` 67 | 68 | There are plenty more methods to explore! 69 | ```python 70 | from slist import Slist 71 | 72 | result = ( 73 | Slist([1, 2, 3]) 74 | .repeat_until_size_or_raise(20) 75 | .grouped(2) 76 | .map(lambda inner_list: inner_list[0] + inner_list[1] if inner_list.length == 2 else inner_list[0]) 77 | .flatten_option() 78 | .distinct_by(lambda x: x) 79 | .map(str) 80 | .reversed() 81 | .mk_string(sep=",") 82 | ) 83 | assert result == "5,4,3" 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit 4 | helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at https://github.com/thejaminator/slist/issues. 13 | 14 | If you are reporting a bug, please include: 15 | 16 | * Your operating system name and version. 17 | * Any details about your local setup that might be helpful in troubleshooting. 18 | * Detailed steps to reproduce the bug. 19 | 20 | ### Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 23 | wanted" is open to whoever wants to implement it. 24 | 25 | ### Implement Features 26 | 27 | Look through the GitHub issues for features. Anything tagged with "enhancement" 28 | and "help wanted" is open to whoever wants to implement it. 29 | 30 | ### Write Documentation 31 | 32 | slist could always use more documentation, whether as part of the 33 | official slist docs, in docstrings, or even on the web in blog posts, 34 | articles, and such. 35 | 36 | ### Submit Feedback 37 | 38 | The best way to send feedback is to file an issue at https://github.com/thejaminator/slist/issues. 39 | 40 | If you are proposing a feature: 41 | 42 | * Explain in detail how it would work. 43 | * Keep the scope as narrow as possible, to make it easier to implement. 44 | * Remember that this is a volunteer-driven project, and that contributions 45 | are welcome :) 46 | 47 | ## Get Started! 48 | 49 | Ready to contribute? Here's how to set up `slist` for local development. 50 | 51 | 1. Fork the `slist` repo on GitHub. 52 | 2. Clone your fork locally 53 | 54 | ``` 55 | $ git clone git@github.com:your_name_here/slist.git 56 | ``` 57 | 58 | 3. Ensure [poetry](https://python-poetry.org/docs/) is installed. 59 | 4. Install dependencies and start your virtualenv: 60 | 61 | ``` 62 | $ poetry install -E test -E doc -E dev 63 | $ pip install tox 64 | ``` 65 | 66 | 5. Create a branch for local development: 67 | 68 | ``` 69 | $ git checkout -b name-of-your-bugfix-or-feature 70 | ``` 71 | 72 | Now you can make your changes locally. 73 | 74 | 6. When you're done making changes, check that your changes pass the 75 | tests, including testing other Python versions, with tox: 76 | 77 | ``` 78 | $ poetry run pytest tests 79 | ``` 80 | 81 | 7. Commit your changes and push your branch to GitHub: 82 | 83 | ``` 84 | $ git add . 85 | $ git commit -m "Your detailed description of your changes." 86 | $ git push origin name-of-your-bugfix-or-feature 87 | ``` 88 | 89 | 8. Submit a pull request through the GitHub website. 90 | 91 | ## Pull Request Guidelines 92 | 93 | Before you submit a pull request, check that it meets these guidelines: 94 | 95 | 1. The pull request should include tests. 96 | 2. If the pull request adds functionality, the docs should be updated. Put 97 | your new functionality into a function with a docstring, and add the 98 | feature to the list in README.md. 99 | 3. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check 100 | https://github.com/thejaminator/slist/actions 101 | and make sure that the tests pass for all supported Python versions. 102 | 103 | ## Tips 104 | 105 | ``` 106 | $ poetry run pytest tests/test_slist.py 107 | ``` 108 | 109 | To run a subset of tests. 110 | 111 | 112 | ## Deploying 113 | 114 | A reminder for the maintainers on how to deploy. 115 | Make sure all your changes are committed (including an entry in CHANGELOG.md). 116 | Then run: 117 | 118 | ``` 119 | $ poetry run bump2version patch # possible: major / minor / patch 120 | $ git push 121 | $ git push --tags 122 | ``` 123 | 124 | GitHub Actions will then deploy to PyPI if tests pass. 125 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit 4 | helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at https://github.com/thejaminator/slist/issues. 13 | 14 | If you are reporting a bug, please include: 15 | 16 | * Your operating system name and version. 17 | * Any details about your local setup that might be helpful in troubleshooting. 18 | * Detailed steps to reproduce the bug. 19 | 20 | ### Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 23 | wanted" is open to whoever wants to implement it. 24 | 25 | ### Implement Features 26 | 27 | Look through the GitHub issues for features. Anything tagged with "enhancement" 28 | and "help wanted" is open to whoever wants to implement it. 29 | 30 | ### Write Documentation 31 | 32 | slist could always use more documentation, whether as part of the 33 | official slist docs, in docstrings, or even on the web in blog posts, 34 | articles, and such. 35 | 36 | ### Submit Feedback 37 | 38 | The best way to send feedback is to file an issue at https://github.com/thejaminator/slist/issues. 39 | 40 | If you are proposing a feature: 41 | 42 | * Explain in detail how it would work. 43 | * Keep the scope as narrow as possible, to make it easier to implement. 44 | * Remember that this is a volunteer-driven project, and that contributions 45 | are welcome :) 46 | 47 | ## Get Started! 48 | 49 | Ready to contribute? Here's how to set up `slist` for local development. 50 | 51 | 1. Fork the `slist` repo on GitHub. 52 | 2. Clone your fork locally 53 | 54 | ``` 55 | $ git clone git@github.com:your_name_here/slist.git 56 | ``` 57 | 58 | 3. Ensure [poetry](https://python-poetry.org/docs/) is installed. 59 | 4. Install dependencies and start your virtualenv: 60 | 61 | ``` 62 | $ poetry install -E test -E doc -E dev 63 | $ pip install tox 64 | ``` 65 | 66 | 5. Create a branch for local development: 67 | 68 | ``` 69 | $ git checkout -b name-of-your-bugfix-or-feature 70 | ``` 71 | 72 | Now you can make your changes locally. 73 | 74 | 6. When you're done making changes, check that your changes pass the 75 | tests, including testing other Python versions, with tox: 76 | 77 | ``` 78 | $ poetry run pytest tests 79 | ``` 80 | 81 | 7. Commit your changes and push your branch to GitHub: 82 | 83 | ``` 84 | $ git add . 85 | $ git commit -m "Your detailed description of your changes." 86 | $ git push origin name-of-your-bugfix-or-feature 87 | ``` 88 | 89 | 8. Submit a pull request through the GitHub website. 90 | 91 | ## Pull Request Guidelines 92 | 93 | Before you submit a pull request, check that it meets these guidelines: 94 | 95 | 1. The pull request should include tests. 96 | 2. If the pull request adds functionality, the docs should be updated. Put 97 | your new functionality into a function with a docstring, and add the 98 | feature to the list in README.md. 99 | 3. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check 100 | https://github.com/thejaminator/slist/actions 101 | and make sure that the tests pass for all supported Python versions. 102 | 103 | ## Tips 104 | 105 | ``` 106 | $ poetry run pytest tests/test_slist.py 107 | ``` 108 | 109 | To run a subset of tests. 110 | 111 | 112 | ## Deploying 113 | 114 | A reminder for the maintainers on how to deploy. 115 | Make sure all your changes are committed (including an entry in CHANGELOG.md). 116 | Then run: 117 | 118 | ``` 119 | $ poetry run bump2version patch # possible: major / minor / patch 120 | $ git push 121 | $ git push --tags 122 | ``` 123 | 124 | GitHub Actions will then deploy to PyPI if tests pass. 125 | 126 | ## Documentation Guidelines 127 | 128 | ### Docstring Format 129 | All new code should include docstrings following the reST/NumPy format: 130 | 131 | ```python 132 | def method_name(self, param1: Type1, param2: Type2) -> ReturnType: 133 | """Short description of what the method does. 134 | 135 | Parameters 136 | ---------- 137 | param1 : Type1 138 | Description of param1 139 | param2 : Type2 140 | Description of param2 141 | 142 | Returns 143 | ------- 144 | ReturnType 145 | Description of return value 146 | 147 | Examples 148 | -------- 149 | >>> Slist([1, 2, 3]).method_name(param1, param2) 150 | Expected output 151 | """ 152 | ``` 153 | 154 | ### Documentation Requirements 155 | 156 | 1. All public methods must have docstrings 157 | 2. Include type information in Parameters and Returns sections 158 | 3. Provide at least one working example 159 | 4. Use backticks (`` ` ``) for inline code references 160 | 5. Keep examples simple and focused on one use case 161 | 6. Include edge cases in examples where relevant 162 | 163 | ### Building Documentation 164 | 165 | 1. Install documentation dependencies: 166 | ```bash 167 | poetry install -E doc 168 | ``` 169 | 170 | 2. Preview documentation locally: 171 | ```bash 172 | mkdocs serve 173 | ``` 174 | 175 | 3. Build documentation: 176 | ```bash 177 | mkdocs build 178 | ``` 179 | 180 | The documentation will automatically be built and deployed when changes are merged to main. 181 | 182 | ### Documentation Structure 183 | 184 | - `docs/index.md`: Main landing page and quick start 185 | - `docs/api/`: API reference documentation 186 | - `docs/contributing.md`: Contribution guidelines (this file) 187 | 188 | ### Tips for Good Documentation 189 | 190 | 1. Write clear, concise descriptions 191 | 2. Include both basic and advanced examples 192 | 3. Document exceptions and edge cases 193 | 4. Keep examples runnable and tested 194 | 5. Update docs when changing method signatures 195 | 6. Use proper formatting for code blocks and inline code 196 | -------------------------------------------------------------------------------- /tests/test_slist.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import CaptureFixture 3 | 4 | from slist import Slist, identity, Group 5 | import numpy as np 6 | 7 | 8 | def test_split_proportion(): 9 | test_list = Slist([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 10 | left, right = test_list.split_proportion(0.5) 11 | assert left == Slist([1, 2, 3, 4, 5]) 12 | assert right == Slist([6, 7, 8, 9, 10]) 13 | 14 | left, right = test_list.split_proportion(0.25) 15 | assert left == Slist([1, 2, 3]) 16 | assert right == Slist([4, 5, 6, 7, 8, 9, 10]) 17 | 18 | left, right = test_list.split_proportion(0.75) 19 | assert left == Slist([1, 2, 3, 4, 5, 6, 7, 8]) 20 | assert right == Slist([9, 10]) 21 | 22 | 23 | def test_repeat_until_size(): 24 | assert Slist([]).repeat_until_size(5) is None, "should give back empty list" 25 | assert Slist([1, 2, 3]).repeat_until_size(5) == Slist([1, 2, 3, 1, 2]), "should repeat 1 and 2" 26 | assert Slist([1, 2, 3, 4, 5, 6]).repeat_until_size(5) == Slist([1, 2, 3, 4, 5]), "should be truncated" 27 | 28 | 29 | def test_repeat_until_size_enumerate(): 30 | with pytest.raises(ValueError): 31 | Slist([]).repeat_until_size_enumerate(5) # Empty list should raise ValueError 32 | 33 | result = Slist([1, 2, 3]).repeat_until_size_enumerate(5) 34 | assert result == Slist([(0, 1), (0, 2), (0, 3), (1, 1), (1, 2)]), "should repeat with repetition count" 35 | 36 | result = Slist(["a"]).repeat_until_size_enumerate(3) 37 | assert result == Slist([(0, "a"), (1, "a"), (2, "a")]), "single element repeated with increasing count" 38 | 39 | # Test with exact size match 40 | result = Slist([1, 2]).repeat_until_size_enumerate(4) 41 | assert result == Slist([(0, 1), (0, 2), (1, 1), (1, 2)]), "should have exact number of elements" 42 | 43 | 44 | def test_split_by(): 45 | assert Slist([]).split_by(lambda x: x % 2 == 0) == ( 46 | Slist([]), 47 | Slist([]), 48 | ), "should split an empty Slist correctly into two empty Slists" 49 | assert Slist([1, 2, 3, 4, 5]).split_by(lambda x: x % 2 == 0) == ( 50 | Slist([2, 4]), 51 | Slist([1, 3, 5]), 52 | ), "should split a non-empty Slist correctly into two Slists" 53 | assert ( 54 | Slist([1, 2, 3, 4, 5]).split_by(lambda x: True) 55 | == ( 56 | Slist([1, 2, 3, 4, 5]), 57 | Slist([]), 58 | ) 59 | ), "should split a non-empty Slist with an always True predicate with all elements in left Slist, and no elements on right Slist" 60 | assert ( 61 | Slist([1, 2, 3, 4, 5]).split_by(lambda x: False) 62 | == ( 63 | Slist([]), 64 | Slist([1, 2, 3, 4, 5]), 65 | ) 66 | ), "should split a non-empty Slist with an always True predicate with all elements in left Slist, and no elements on right Slist" 67 | 68 | 69 | def test_split_on(): 70 | assert Slist([1, 2, 3, 4, 5]).split_on(lambda x: x == 3) == Slist( 71 | [ 72 | Slist([1, 2]), 73 | Slist([4, 5]), 74 | ] 75 | ) 76 | assert Slist(["hello", "", "world"]).split_on(lambda x: x == "") == Slist( 77 | [ 78 | Slist(["hello"]), 79 | Slist(["world"]), 80 | ] 81 | ) 82 | assert Slist(["hello", "world"]).split_on(lambda x: x == "") == Slist( 83 | [ 84 | Slist(["hello", "world"]), 85 | ] 86 | ) 87 | 88 | 89 | def test_find_last_idx_or_raise(): 90 | assert Slist([1, 1, 1, 1]).find_last_idx_or_raise(lambda x: x == 1) == 3 91 | 92 | 93 | def test_zip(): 94 | assert Slist([]).zip(Slist([])) == Slist([]) 95 | assert Slist([1, 2, 3]).zip(Slist(["1", "2", "3"])) == Slist([(1, "1"), (2, "2"), (3, "3")]) 96 | assert Slist([1, 2, 3]).zip(Slist(["1", "2", "3"]), Slist([True, True, True])) == Slist( 97 | [(1, "1", True), (2, "2", True), (3, "3", True)] 98 | ) 99 | 100 | with pytest.raises(ValueError): 101 | Slist([1, 2, 3]).zip(Slist(["1"])) 102 | with pytest.raises(ValueError): 103 | Slist([1, 2, 3]).zip(Slist(["1", "2", "3"]), Slist(["1"])) 104 | 105 | 106 | def test_zip_cycle(): 107 | # Test empty lists 108 | assert Slist([]).zip_cycle(Slist([])) == Slist([]) 109 | assert Slist([1, 2, 3]).zip_cycle(Slist([])) == Slist([]) 110 | assert Slist([]).zip_cycle(Slist([1, 2, 3])) == Slist([]) 111 | 112 | # Test first sequence longer 113 | assert Slist([1, 2, 3]).zip_cycle(["a", "b"]) == Slist([(1, "a"), (2, "b"), (3, "a")]) 114 | 115 | # Test second sequence longer 116 | assert Slist([1, 2]).zip_cycle(["a", "b", "c", "d"]) == Slist([(1, "a"), (2, "b"), (1, "c"), (2, "d")]) 117 | 118 | # Test equal length sequences 119 | assert Slist([1, 2, 3]).zip_cycle(["a", "b", "c"]) == Slist([(1, "a"), (2, "b"), (3, "c")]) 120 | 121 | # Test with three sequences of different lengths 122 | assert Slist([1, 2, 3]).zip_cycle(["a", "b"], [10, 20, 30, 40]) == Slist( 123 | [(1, "a", 10), (2, "b", 20), (3, "a", 30), (1, "b", 40)] 124 | ) 125 | 126 | # Test with single element cycling 127 | assert Slist([1]).zip_cycle(["a", "b", "c"]) == Slist([(1, "a"), (1, "b"), (1, "c")]) 128 | assert Slist([1, 2, 3]).zip_cycle(["a"]) == Slist([(1, "a"), (2, "a"), (3, "a")]) 129 | 130 | # Test longer sequence to verify proper cycling 131 | assert Slist([1, 2]).zip_cycle(["a", "b", "c", "d", "e", "f"]) == Slist( 132 | [(1, "a"), (2, "b"), (1, "c"), (2, "d"), (1, "e"), (2, "f")] 133 | ) 134 | 135 | 136 | def test_take_until_inclusive(): 137 | assert Slist([]).take_until_inclusive(lambda x: x == 1) == Slist([]) 138 | assert Slist([1, 2, 3]).take_until_inclusive(lambda x: x == 1) == Slist([1]) 139 | assert Slist([1, 2, 3]).take_until_inclusive(lambda x: x == 2) == Slist([1, 2]) 140 | assert Slist([1, 2, 3]).take_until_inclusive(lambda x: x == 3) == Slist([1, 2, 3]) 141 | assert Slist([1, 2, 3]).take_until_inclusive(lambda x: x == 4) == Slist([1, 2, 3]) 142 | assert Slist([1, 2, 3]).take_until_inclusive(lambda x: x == 5) == Slist([1, 2, 3]) 143 | 144 | 145 | @pytest.mark.asyncio 146 | async def test_par_map_async(): 147 | async def func(x: int) -> int: 148 | return x * 2 149 | 150 | result = await Slist([1, 2, 3]).par_map_async(func) 151 | assert result == Slist([2, 4, 6]) 152 | 153 | 154 | @pytest.mark.asyncio 155 | async def test_par_map_async_max_parallel(): 156 | async def func(x: int) -> int: 157 | return x * 2 158 | 159 | result = await Slist([1, 2, 3]).par_map_async(func, max_par=1) 160 | assert result == Slist([2, 4, 6]) 161 | 162 | 163 | @pytest.mark.asyncio 164 | async def test_par_map_async_max_parallel_tqdm(): 165 | async def func(x: int) -> int: 166 | return x * 2 167 | 168 | result = await Slist([1, 2, 3]).par_map_async(func, max_par=1, tqdm=True) 169 | assert result == Slist([2, 4, 6]) 170 | 171 | 172 | def test_grouped(): 173 | test_list = Slist([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 174 | assert test_list.grouped(2) == Slist([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]) 175 | test_list_2 = Slist([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) 176 | assert test_list_2.grouped(2) == Slist([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]) 177 | 178 | 179 | def test_window(): 180 | test_list = Slist([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 181 | assert test_list.window(size=1) == Slist([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]) 182 | assert test_list.window(size=2) == Slist([[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]) 183 | 184 | 185 | def test_window_empty_list(): 186 | test_list = Slist([]) 187 | assert test_list.window(size=1) == Slist() 188 | assert test_list.window(size=2) == Slist() 189 | 190 | 191 | def test_window_too_small_list(): 192 | test_list = Slist([1]) 193 | assert test_list.window(size=2) == Slist() 194 | 195 | 196 | def test_median(): 197 | numbers = Slist([2, 3, 4, 5, 6, 7, 8, 9, 1]) 198 | assert numbers.median_by(identity) == 5 199 | 200 | 201 | def test_percentile(): 202 | numbers = Slist([2, 3, 4, 5, 6, 7, 8, 9, 1]) 203 | assert numbers.percentile_by(identity, 0.5) == 5 204 | assert numbers.percentile_by(identity, 0.25) == 3 205 | assert numbers.percentile_by(identity, 0.75) == 7 206 | 207 | 208 | def test_max_by(): 209 | numbers = Slist([2, 3, 4, 5, 6, 7, 8, 9, 1]) 210 | assert numbers.max_by(identity) == 9 211 | empty = Slist([]) 212 | assert empty.max_by(identity) is None 213 | 214 | 215 | def test_max_option(): 216 | numbers = Slist([2, 3, 4, 5, 6, 7, 8, 9, 1]) 217 | assert numbers.max_option() == 9 218 | empty = Slist([]) 219 | assert empty.max_option() is None 220 | 221 | 222 | def test_min_by(): 223 | numbers = Slist([2, 3, 4, 5, 6, 7, 8, 9, 1]) 224 | assert numbers.min_by(identity) == 1 225 | empty = Slist([]) 226 | assert empty.min_by(identity) is None 227 | 228 | 229 | def test_mode_option(): 230 | numbers = Slist([1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1]) 231 | assert numbers.mode_option == 1 232 | empty = Slist([]) 233 | assert empty.mode_option is None 234 | 235 | 236 | def test_fold_left_add(): 237 | numbers = Slist([1, 2, 3, 4, 5]) 238 | assert numbers.sum_option() == 15 239 | empty = Slist([]) 240 | assert empty.sum_option() is None 241 | 242 | 243 | def test_group_by(): 244 | class Animal: 245 | def __init__(self, name: str, age: int): 246 | self.name = name 247 | self.age = age 248 | 249 | animals = Slist( 250 | [ 251 | Animal("cat", 1), 252 | Animal("cat", 2), 253 | Animal("dog", 1), 254 | ] 255 | ) 256 | # group_by name, then average out the age 257 | result = animals.group_by(lambda animal: animal.name).map( 258 | lambda group: group.map_values(lambda animal: animal.map(lambda x: x.age).average_or_raise()) 259 | ) 260 | assert result == Slist( 261 | [ 262 | ("cat", 1.5), 263 | ("dog", 1), 264 | ] 265 | ) 266 | 267 | 268 | def test_group_by_len(): 269 | class Animal: 270 | def __init__(self, name: str, age: int): 271 | self.name = name 272 | self.age = age 273 | 274 | animals = Slist( 275 | [ 276 | Animal("cat", 1), 277 | Animal("cat", 2), 278 | Animal("dog", 1), 279 | ] 280 | ) 281 | # group_by name, then average out the age 282 | result = animals.group_by(lambda animal: animal.name).map(lambda group: group.map_values(len)) 283 | assert result == Slist( 284 | [ 285 | ("cat", 2), 286 | ("dog", 1), 287 | ] 288 | ) 289 | 290 | 291 | def test_group_by_map_2(): 292 | class Animal: 293 | def __init__(self, name: str, age: int): 294 | self.name = name 295 | self.age = age 296 | 297 | animals = Slist( 298 | [ 299 | Animal("cat", 1), 300 | Animal("cat", 2), 301 | Animal("dog", 1), 302 | ] 303 | ) 304 | # group_by name, then average out the age 305 | group = animals.group_by(lambda animal: animal.name) 306 | result = group.map_2( 307 | lambda group_name, group_values: (group_name, group_values.map(lambda animal: animal.age).average_or_raise()) 308 | ) 309 | assert result == Slist( 310 | [ 311 | ("cat", 1.5), 312 | ("dog", 1), 313 | ] 314 | ) 315 | 316 | 317 | def test_take_or_raise(): 318 | numbers = Slist([1, 2, 3, 4, 5]) 319 | assert numbers.take_or_raise(0) == Slist([]) 320 | assert numbers.take_or_raise(1) == Slist([1]) 321 | assert numbers.take_or_raise(2) == Slist([1, 2]) 322 | assert numbers.take_or_raise(5) == Slist([1, 2, 3, 4, 5]) 323 | with pytest.raises(ValueError): 324 | numbers.take_or_raise(6) 325 | 326 | 327 | def test_product(): 328 | numbers = Slist([1, 2, 3, 4, 5]) 329 | # cartesian product 330 | assert numbers.product(numbers) == Slist( 331 | [ 332 | (1, 1), 333 | (1, 2), 334 | (1, 3), 335 | (1, 4), 336 | (1, 5), 337 | (2, 1), 338 | (2, 2), 339 | (2, 3), 340 | (2, 4), 341 | (2, 5), 342 | (3, 1), 343 | (3, 2), 344 | (3, 3), 345 | (3, 4), 346 | (3, 5), 347 | (4, 1), 348 | (4, 2), 349 | (4, 3), 350 | (4, 4), 351 | (4, 5), 352 | (5, 1), 353 | (5, 2), 354 | (5, 3), 355 | (5, 4), 356 | (5, 5), 357 | ] 358 | ) 359 | 360 | 361 | def test_statistics_or_raise(): 362 | numbers = Slist([1, 2, 3, 4, 5]) 363 | results = numbers.statistics_or_raise() 364 | assert results.average == 3 365 | assert results.count == 5 366 | 367 | # convert the above to use numpy roughly equal 368 | assert np.isclose(results.upper_confidence_interval_95, 4.38, atol=0.01) 369 | assert np.isclose(results.lower_confidence_interval_95, 1.61, atol=0.01) 370 | 371 | empty = Slist([]) 372 | with pytest.raises(ValueError): 373 | empty.statistics_or_raise() 374 | 375 | 376 | def test_one(): 377 | assert Slist.one(1) == Slist([1]) 378 | assert Slist.one("test") == Slist(["test"]) 379 | assert Slist.one([1, 2]) == Slist([[1, 2]]) 380 | 381 | 382 | def test_one_option(): 383 | assert Slist.one_option(1) == Slist([1]) 384 | assert Slist.one_option(None) == Slist() 385 | assert Slist.one_option("test") == Slist(["test"]) 386 | 387 | 388 | def test_any(): 389 | numbers = Slist([1, 2, 3, 4, 5]) 390 | assert numbers.any(lambda x: x > 3) is True 391 | assert numbers.any(lambda x: x > 5) is False 392 | assert Slist([]).any(lambda x: True) is False 393 | 394 | 395 | def test_all(): 396 | numbers = Slist([1, 2, 3, 4, 5]) 397 | assert numbers.all(lambda x: x > 0) is True 398 | assert numbers.all(lambda x: x > 3) is False 399 | assert Slist([]).all(lambda x: False) is True 400 | 401 | 402 | def test_max_by_ordering(): 403 | numbers = Slist([1, 2, 3, 4, 5]) 404 | # Find maximum by comparing if first number is less than second 405 | assert numbers.max_by_ordering(lambda x, y: x < y) == 5 406 | # Find maximum by comparing if first number is greater than second (will give minimum) 407 | assert numbers.max_by_ordering(lambda x, y: x > y) == 1 408 | assert Slist([]).max_by_ordering(lambda x, y: x < y) is None 409 | 410 | 411 | def test_slice_with_bool(): 412 | numbers = Slist([1, 2, 3, 4, 5]) 413 | bools = [True, False, True, False, True] 414 | assert numbers.slice_with_bool(bools) == Slist([1, 3, 5]) 415 | assert numbers.slice_with_bool([False, False, False, False, False]) == Slist([]) 416 | assert numbers.slice_with_bool([True, True, True, True, True]) == numbers 417 | 418 | 419 | def test_find_one_or_raise(): 420 | numbers = Slist([1, 2, 3, 4, 5]) 421 | assert numbers.find_one_or_raise(lambda x: x > 3) == 4 422 | assert numbers.find_one_or_raise(lambda x: x == 1) == 1 423 | with pytest.raises(RuntimeError, match="Failed to find predicate"): 424 | numbers.find_one_or_raise(lambda x: x > 10) 425 | with pytest.raises(ValueError, match="Custom error"): 426 | numbers.find_one_or_raise(lambda x: x > 10, ValueError("Custom error")) 427 | 428 | 429 | def test_pairwise(): 430 | numbers = Slist([1, 2, 3, 4, 5]) 431 | assert numbers.pairwise() == Slist([(1, 2), (2, 3), (3, 4), (4, 5)]) 432 | assert Slist([1]).pairwise() == Slist([]) 433 | assert Slist([]).pairwise() == Slist([]) 434 | 435 | 436 | def test_print_length(capsys: CaptureFixture[str]): 437 | numbers = Slist([1, 2, 3, 4, 5]) 438 | result = numbers.print_length() 439 | captured = capsys.readouterr() 440 | assert captured.out == "Slist Length: 5\n" 441 | assert result == numbers # Should return the original list 442 | 443 | # Test with custom prefix 444 | result = numbers.print_length(prefix="Length: ") 445 | captured = capsys.readouterr() 446 | assert captured.out == "Length: 5\n" 447 | 448 | # Test with custom printer 449 | output = [] 450 | numbers.print_length(printer=output.append) 451 | assert output == ["Slist Length: 5"] 452 | 453 | 454 | def test_empty_properties(): 455 | empty_list = Slist([]) 456 | assert empty_list.is_empty is True 457 | assert empty_list.not_empty is False 458 | 459 | non_empty_list = Slist([1, 2, 3]) 460 | assert non_empty_list.is_empty is False 461 | assert non_empty_list.not_empty is True 462 | 463 | 464 | def test_value_counts(): 465 | # Test with strings 466 | data = Slist(["apple", "banana", "apple", "cherry", "banana", "apple"]) 467 | result = data.value_counts(key=lambda x: x) 468 | assert result == Slist([Group(key="apple", values=3), Group(key="banana", values=2), Group(key="cherry", values=1)]) 469 | 470 | # Test with sort=False should preserve input order 471 | data = Slist(["cherry", "banana", "apple", "banana", "apple", "apple"]) 472 | result = data.value_counts(key=lambda x: x, sort=False) 473 | assert result == Slist([Group(key="cherry", values=1), Group(key="banana", values=2), Group(key="apple", values=3)]) 474 | 475 | # Test with numbers 476 | numbers = Slist([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]) 477 | result = numbers.value_counts(key=lambda x: x) 478 | assert result == Slist( 479 | [Group(key=4, values=4), Group(key=3, values=3), Group(key=2, values=2), Group(key=1, values=1)] 480 | ) 481 | 482 | # Test with custom key function 483 | data = Slist(["apple", "banana", "cherry", "date", "elderberry"]) 484 | result = data.value_counts(key=lambda x: len(x)) 485 | assert result == Slist( 486 | [Group(key=6, values=2), Group(key=5, values=1), Group(key=4, values=1), Group(key=10, values=1)] 487 | ) 488 | 489 | # Test with empty list 490 | empty = Slist([]) 491 | result = empty.value_counts(key=lambda x: x) 492 | assert result == Slist([]) 493 | 494 | 495 | def test_value_percentage(): 496 | # Test with strings 497 | data = Slist(["apple", "banana", "apple", "cherry", "banana", "apple"]) 498 | result = data.value_percentage(key=lambda x: x) 499 | 500 | assert len(result) == 3 501 | assert result[0].key == "apple" 502 | assert result[1].key == "banana" 503 | assert result[2].key == "cherry" 504 | assert pytest.approx(result[0].values) == 0.5 # 3/6 505 | assert pytest.approx(result[1].values) == 1 / 3 # 2/6 506 | assert pytest.approx(result[2].values) == 1 / 6 # 1/6 507 | 508 | # Test with sort=False should preserve input order 509 | data = Slist(["cherry", "banana", "apple", "banana", "apple", "apple"]) 510 | result = data.value_percentage(key=lambda x: x, sort=False) 511 | 512 | assert len(result) == 3 513 | assert result[0].key == "cherry" 514 | assert result[1].key == "banana" 515 | assert result[2].key == "apple" 516 | assert pytest.approx(result[0].values) == 1 / 6 # 1/6 517 | assert pytest.approx(result[1].values) == 1 / 3 # 2/6 518 | assert pytest.approx(result[2].values) == 0.5 # 3/6 519 | 520 | # Test with numbers 521 | numbers = Slist([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]) 522 | result = numbers.value_percentage(key=lambda x: x) 523 | 524 | assert len(result) == 4 525 | assert result[0].key == 4 526 | assert result[1].key == 3 527 | assert result[2].key == 2 528 | assert result[3].key == 1 529 | assert pytest.approx(result[0].values) == 0.4 # 4/10 530 | assert pytest.approx(result[1].values) == 0.3 # 3/10 531 | assert pytest.approx(result[2].values) == 0.2 # 2/10 532 | assert pytest.approx(result[3].values) == 0.1 # 1/10 533 | 534 | # Test with custom key function 535 | data = Slist(["apple", "banana", "cherry", "date", "elderberry"]) 536 | result = data.value_percentage(key=lambda x: len(x)) 537 | 538 | assert len(result) == 4 539 | assert "date" in data # 4 chars 540 | assert "apple" in data # 5 chars 541 | assert "banana" in data and "cherry" in data # 6 chars 542 | assert "elderberry" in data # 10 chars 543 | 544 | # Verify all keys are present 545 | keys = [group.key for group in result] 546 | assert 4 in keys 547 | assert 5 in keys 548 | assert 6 in keys 549 | assert 10 in keys 550 | 551 | # Verify values add up to 1.0 552 | total = sum(group.values for group in result) 553 | assert pytest.approx(total) == 1.0 554 | 555 | # Verify each value is 0.2 (1/5) except for length 6 which should be 0.4 (2/5) 556 | for group in result: 557 | if group.key == 6: 558 | assert pytest.approx(group.values) == 0.4 # 2/5 559 | else: 560 | assert pytest.approx(group.values) == 0.2 # 1/5 561 | 562 | # Test with empty list 563 | empty = Slist([]) 564 | result = empty.value_percentage(key=lambda x: x) 565 | assert result == Slist([]) 566 | 567 | 568 | def test_permutations_pairs(): 569 | # Test with a list of integers 570 | s = Slist([1, 2, 3]) 571 | result = s.permutations_pairs() 572 | expected = [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)] 573 | assert sorted(result) == sorted(expected) 574 | 575 | # Test with empty list 576 | assert Slist([]).permutations_pairs() == [] 577 | 578 | # Test with single item 579 | assert Slist([1]).permutations_pairs() == [] 580 | 581 | # Test with duplicates 582 | s = Slist([1, 1, 2]) 583 | result = s.permutations_pairs() 584 | expected = [(1, 1), (1, 2), (1, 1), (1, 2), (2, 1), (2, 1)] 585 | assert sorted(result) == sorted(expected) 586 | 587 | 588 | def test_combinations_pairs(): 589 | # Test with a list of integers 590 | s = Slist([1, 2, 3]) 591 | result = s.combinations_pairs() 592 | expected = [(1, 2), (1, 3), (2, 3)] 593 | assert sorted(result) == sorted(expected) 594 | 595 | # Test with empty list 596 | assert Slist([]).combinations_pairs() == [] 597 | 598 | # Test with single item 599 | assert Slist([1]).combinations_pairs() == [] 600 | 601 | # Test with duplicates 602 | s = Slist([1, 1, 2]) 603 | result = s.combinations_pairs() 604 | expected = [(1, 1), (1, 2), (1, 2)] 605 | assert sorted(result) == sorted(expected) 606 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appdirs" 5 | version = "1.4.4" 6 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 7 | optional = true 8 | python-versions = "*" 9 | files = [ 10 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 11 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 12 | ] 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.1" 17 | description = "Atomic file writes." 18 | optional = true 19 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 20 | files = [ 21 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, 22 | ] 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "24.3.0" 27 | description = "Classes Without Boilerplate" 28 | optional = true 29 | python-versions = ">=3.8" 30 | files = [ 31 | {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, 32 | {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, 33 | ] 34 | 35 | [package.extras] 36 | benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 37 | cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 38 | dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 39 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 40 | tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 41 | tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] 42 | 43 | [[package]] 44 | name = "black" 45 | version = "21.6b0" 46 | description = "The uncompromising code formatter." 47 | optional = true 48 | python-versions = ">=3.6.2" 49 | files = [ 50 | {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, 51 | {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, 52 | ] 53 | 54 | [package.dependencies] 55 | appdirs = "*" 56 | click = ">=7.1.2" 57 | mypy-extensions = ">=0.4.3" 58 | pathspec = ">=0.8.1,<1" 59 | regex = ">=2020.1.8" 60 | toml = ">=0.10.1" 61 | 62 | [package.extras] 63 | colorama = ["colorama (>=0.4.3)"] 64 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 65 | python2 = ["typed-ast (>=1.4.2)"] 66 | uvloop = ["uvloop (>=0.15.2)"] 67 | 68 | [[package]] 69 | name = "click" 70 | version = "8.1.8" 71 | description = "Composable command line interface toolkit" 72 | optional = true 73 | python-versions = ">=3.7" 74 | files = [ 75 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 76 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 77 | ] 78 | 79 | [package.dependencies] 80 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 81 | 82 | [[package]] 83 | name = "colorama" 84 | version = "0.4.6" 85 | description = "Cross-platform colored terminal text." 86 | optional = true 87 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 88 | files = [ 89 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 90 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 91 | ] 92 | 93 | [[package]] 94 | name = "coverage" 95 | version = "7.6.1" 96 | description = "Code coverage measurement for Python" 97 | optional = true 98 | python-versions = ">=3.8" 99 | files = [ 100 | {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, 101 | {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, 102 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, 103 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, 104 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, 105 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, 106 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, 107 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, 108 | {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, 109 | {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, 110 | {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, 111 | {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, 112 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, 113 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, 114 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, 115 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, 116 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, 117 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, 118 | {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, 119 | {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, 120 | {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, 121 | {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, 122 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, 123 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, 124 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, 125 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, 126 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, 127 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, 128 | {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, 129 | {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, 130 | {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, 131 | {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, 132 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, 133 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, 134 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, 135 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, 136 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, 137 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, 138 | {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, 139 | {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, 140 | {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, 141 | {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, 142 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, 143 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, 144 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, 145 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, 146 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, 147 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, 148 | {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, 149 | {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, 150 | {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, 151 | {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, 152 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, 153 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, 154 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, 155 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, 156 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, 157 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, 158 | {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, 159 | {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, 160 | {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, 161 | {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, 162 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, 163 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, 164 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, 165 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, 166 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, 167 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, 168 | {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, 169 | {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, 170 | {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, 171 | {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, 172 | ] 173 | 174 | [package.extras] 175 | toml = ["tomli"] 176 | 177 | [[package]] 178 | name = "distlib" 179 | version = "0.3.9" 180 | description = "Distribution utilities" 181 | optional = true 182 | python-versions = "*" 183 | files = [ 184 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 185 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 186 | ] 187 | 188 | [[package]] 189 | name = "filelock" 190 | version = "3.16.1" 191 | description = "A platform independent file lock." 192 | optional = true 193 | python-versions = ">=3.8" 194 | files = [ 195 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 196 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 197 | ] 198 | 199 | [package.extras] 200 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 201 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 202 | typing = ["typing-extensions (>=4.12.2)"] 203 | 204 | [[package]] 205 | name = "iniconfig" 206 | version = "2.0.0" 207 | description = "brain-dead simple config-ini parsing" 208 | optional = true 209 | python-versions = ">=3.7" 210 | files = [ 211 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 212 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 213 | ] 214 | 215 | [[package]] 216 | name = "mypy" 217 | version = "0.900" 218 | description = "Optional static typing for Python" 219 | optional = true 220 | python-versions = ">=3.5" 221 | files = [ 222 | {file = "mypy-0.900-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:07efc88486877d595cca7f7d237e6d04d1ba6f01dc8f74a81b716270f6770968"}, 223 | {file = "mypy-0.900-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:80c96f97de241ee7383cfe646bfc51113a48089d50c33275af0033b98dee3b1c"}, 224 | {file = "mypy-0.900-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0e703c0afe36511746513d168e1d2a52f88e2a324169b87a6b6a58901c3afcf3"}, 225 | {file = "mypy-0.900-cp35-cp35m-win_amd64.whl", hash = "sha256:23100137579d718cd6f05d572574ca00701fa2bfc7b645ebc5130d93e2af3bee"}, 226 | {file = "mypy-0.900-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:468b3918b26f81d003e8e9b788c62160acb885487cf4d83a3f22ba9061cb49e2"}, 227 | {file = "mypy-0.900-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d90c296cd5cdef86e720a0994d41d72c06d6ff8ab8fc6aaaf0ee6c675835d596"}, 228 | {file = "mypy-0.900-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:42d66b3d716fe5e22b32915d1fa59e7183a0e02f00b337b834a596c1f5e37f01"}, 229 | {file = "mypy-0.900-cp36-cp36m-win_amd64.whl", hash = "sha256:a354613b4cc6e0e9f1ba7811083cd8f63ccee97333d1df7594c438399c83249a"}, 230 | {file = "mypy-0.900-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:22f97de97373dd6180c4abee90b20c60780820284d2cdc5579927c0e37854cf6"}, 231 | {file = "mypy-0.900-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e75f0c97cfe8d86da89b22ad7039f5af44b8f6b0af12bd2877791a92b4b9e987"}, 232 | {file = "mypy-0.900-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:41f082275a20e3eea48364915f7bc6ec5338be89db1ed8b2e570b9e3d12d4dc6"}, 233 | {file = "mypy-0.900-cp37-cp37m-win_amd64.whl", hash = "sha256:83adbf3f8c5023f4276557fbcb3b6306f9dce01783e8ac5f8c11fcb29f62e899"}, 234 | {file = "mypy-0.900-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2220f97804890f3e6da3f849f81f3e56e367a2027a51dde5ce3b7ebb2ad3342b"}, 235 | {file = "mypy-0.900-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3f1d0601842c6b4248923963fc59a6fdd05dee0fddc8b07e30c508b6a269e68f"}, 236 | {file = "mypy-0.900-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c2f87505840c0f3557ea4aa5893f2459daf6516adac30b15d1d5cf567e0d7939"}, 237 | {file = "mypy-0.900-cp38-cp38-win_amd64.whl", hash = "sha256:a0461da00ed23d17fcb04940db2b72f920435cf79be943564b717e378ffeeddf"}, 238 | {file = "mypy-0.900-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9560b1f572cdaab43fdcdad5ef45138e89dc191729329db1b8ce5636f4cdeacf"}, 239 | {file = "mypy-0.900-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d794f10b9f28d21af7a93054e217872aaf9b9ad1bd354ae5e1a3a923d734b73f"}, 240 | {file = "mypy-0.900-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:68fd1c1c1fc9b405f0ed6cfcd00541de7e83f41007419a125c20fa5db3881cb1"}, 241 | {file = "mypy-0.900-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7eb1e5820deb71e313aa2b5a5220803a9b2e3efa43475537a71d0ffed7495e1e"}, 242 | {file = "mypy-0.900-cp39-cp39-win_amd64.whl", hash = "sha256:6598e39cd5aa1a09d454ad39687b89cf3f3fd7cf1f9c3f81a1a2775f6f6b16f8"}, 243 | {file = "mypy-0.900-py3-none-any.whl", hash = "sha256:3be7c68fab8b318a2d5bcfac8e028dc77b9096ea1ec5594e9866c8fb57ae0296"}, 244 | {file = "mypy-0.900.tar.gz", hash = "sha256:65c78570329c54fb40f956f7645e2359af5da9d8c54baa44f461cdc7f4984108"}, 245 | ] 246 | 247 | [package.dependencies] 248 | mypy-extensions = ">=0.4.3,<0.5.0" 249 | toml = "*" 250 | typing-extensions = ">=3.7.4" 251 | 252 | [package.extras] 253 | dmypy = ["psutil (>=4.0)"] 254 | python2 = ["typed-ast (>=1.4.0,<1.5.0)"] 255 | 256 | [[package]] 257 | name = "mypy-extensions" 258 | version = "0.4.4" 259 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 260 | optional = true 261 | python-versions = ">=2.7" 262 | files = [ 263 | {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, 264 | ] 265 | 266 | [[package]] 267 | name = "numpy" 268 | version = "1.24.4" 269 | description = "Fundamental package for array computing in Python" 270 | optional = true 271 | python-versions = ">=3.8" 272 | files = [ 273 | {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, 274 | {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, 275 | {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, 276 | {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, 277 | {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, 278 | {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, 279 | {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, 280 | {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, 281 | {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, 282 | {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, 283 | {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, 284 | {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, 285 | {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, 286 | {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, 287 | {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, 288 | {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, 289 | {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, 290 | {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, 291 | {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, 292 | {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, 293 | {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, 294 | {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, 295 | {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, 296 | {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, 297 | {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, 298 | {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, 299 | {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, 300 | {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, 301 | ] 302 | 303 | [[package]] 304 | name = "packaging" 305 | version = "24.2" 306 | description = "Core utilities for Python packages" 307 | optional = true 308 | python-versions = ">=3.8" 309 | files = [ 310 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 311 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 312 | ] 313 | 314 | [[package]] 315 | name = "pathspec" 316 | version = "0.12.1" 317 | description = "Utility library for gitignore style pattern matching of file paths." 318 | optional = true 319 | python-versions = ">=3.8" 320 | files = [ 321 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 322 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 323 | ] 324 | 325 | [[package]] 326 | name = "pip" 327 | version = "20.3.4" 328 | description = "The PyPA recommended tool for installing Python packages." 329 | optional = true 330 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 331 | files = [ 332 | {file = "pip-20.3.4-py2.py3-none-any.whl", hash = "sha256:217ae5161a0e08c0fb873858806e3478c9775caffce5168b50ec885e358c199d"}, 333 | {file = "pip-20.3.4.tar.gz", hash = "sha256:6773934e5f5fc3eaa8c5a44949b5b924fc122daa0a8aa9f80c835b4ca2a543fc"}, 334 | ] 335 | 336 | [[package]] 337 | name = "platformdirs" 338 | version = "4.3.6" 339 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 340 | optional = true 341 | python-versions = ">=3.8" 342 | files = [ 343 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 344 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 345 | ] 346 | 347 | [package.extras] 348 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 349 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 350 | type = ["mypy (>=1.11.2)"] 351 | 352 | [[package]] 353 | name = "pluggy" 354 | version = "1.5.0" 355 | description = "plugin and hook calling mechanisms for python" 356 | optional = true 357 | python-versions = ">=3.8" 358 | files = [ 359 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 360 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 361 | ] 362 | 363 | [package.extras] 364 | dev = ["pre-commit", "tox"] 365 | testing = ["pytest", "pytest-benchmark"] 366 | 367 | [[package]] 368 | name = "py" 369 | version = "1.11.0" 370 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 371 | optional = true 372 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 373 | files = [ 374 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 375 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 376 | ] 377 | 378 | [[package]] 379 | name = "pytest" 380 | version = "6.2.5" 381 | description = "pytest: simple powerful testing with Python" 382 | optional = true 383 | python-versions = ">=3.6" 384 | files = [ 385 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 386 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 387 | ] 388 | 389 | [package.dependencies] 390 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 391 | attrs = ">=19.2.0" 392 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 393 | iniconfig = "*" 394 | packaging = "*" 395 | pluggy = ">=0.12,<2.0" 396 | py = ">=1.8.2" 397 | toml = "*" 398 | 399 | [package.extras] 400 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 401 | 402 | [[package]] 403 | name = "pytest-cov" 404 | version = "2.12.1" 405 | description = "Pytest plugin for measuring coverage." 406 | optional = true 407 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 408 | files = [ 409 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 410 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 411 | ] 412 | 413 | [package.dependencies] 414 | coverage = ">=5.2.1" 415 | pytest = ">=4.6" 416 | toml = "*" 417 | 418 | [package.extras] 419 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 420 | 421 | [[package]] 422 | name = "regex" 423 | version = "2024.11.6" 424 | description = "Alternative regular expression module, to replace re." 425 | optional = true 426 | python-versions = ">=3.8" 427 | files = [ 428 | {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, 429 | {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, 430 | {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, 431 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, 432 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, 433 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, 434 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, 435 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, 436 | {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, 437 | {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, 438 | {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, 439 | {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, 440 | {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, 441 | {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, 442 | {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, 443 | {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, 444 | {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, 445 | {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, 446 | {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, 447 | {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, 448 | {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, 449 | {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, 450 | {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, 451 | {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, 452 | {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, 453 | {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, 454 | {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, 455 | {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, 456 | {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, 457 | {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, 458 | {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, 459 | {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, 460 | {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, 461 | {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, 462 | {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, 463 | {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, 464 | {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, 465 | {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, 466 | {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, 467 | {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, 468 | {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, 469 | {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, 470 | {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, 471 | {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, 472 | {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, 473 | {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, 474 | {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, 475 | {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, 476 | {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, 477 | {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, 478 | {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, 479 | {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, 480 | {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, 481 | {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, 482 | {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, 483 | {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, 484 | {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, 485 | {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, 486 | {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, 487 | {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, 488 | {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, 489 | {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, 490 | {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, 491 | {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, 492 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, 493 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, 494 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, 495 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, 496 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, 497 | {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, 498 | {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, 499 | {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, 500 | {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, 501 | {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, 502 | {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, 503 | {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, 504 | {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, 505 | {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, 506 | {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, 507 | {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, 508 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, 509 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, 510 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, 511 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, 512 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, 513 | {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, 514 | {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, 515 | {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, 516 | {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, 517 | {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, 518 | {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, 519 | {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, 520 | {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, 521 | {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, 522 | ] 523 | 524 | [[package]] 525 | name = "six" 526 | version = "1.17.0" 527 | description = "Python 2 and 3 compatibility utilities" 528 | optional = true 529 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 530 | files = [ 531 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 532 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 533 | ] 534 | 535 | [[package]] 536 | name = "toml" 537 | version = "0.10.2" 538 | description = "Python Library for Tom's Obvious, Minimal Language" 539 | optional = true 540 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 541 | files = [ 542 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 543 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 544 | ] 545 | 546 | [[package]] 547 | name = "tomli" 548 | version = "2.2.1" 549 | description = "A lil' TOML parser" 550 | optional = true 551 | python-versions = ">=3.8" 552 | files = [ 553 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 554 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 555 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 556 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 557 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 558 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 559 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 560 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 561 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 562 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 563 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 564 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 565 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 566 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 567 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 568 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 569 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 570 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 571 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 572 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 573 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 574 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 575 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 576 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 577 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 578 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 579 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 580 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 581 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 582 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 583 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 584 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 585 | ] 586 | 587 | [[package]] 588 | name = "tox" 589 | version = "3.28.0" 590 | description = "tox is a generic virtualenv management and test command line tool" 591 | optional = true 592 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 593 | files = [ 594 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 595 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 596 | ] 597 | 598 | [package.dependencies] 599 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 600 | filelock = ">=3.0.0" 601 | packaging = ">=14" 602 | pluggy = ">=0.12.0" 603 | py = ">=1.4.17" 604 | six = ">=1.14.0" 605 | tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} 606 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 607 | 608 | [package.extras] 609 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 610 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] 611 | 612 | [[package]] 613 | name = "tqdm" 614 | version = "4.67.1" 615 | description = "Fast, Extensible Progress Meter" 616 | optional = true 617 | python-versions = ">=3.7" 618 | files = [ 619 | {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, 620 | {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, 621 | ] 622 | 623 | [package.dependencies] 624 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 625 | 626 | [package.extras] 627 | dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] 628 | discord = ["requests"] 629 | notebook = ["ipywidgets (>=6)"] 630 | slack = ["slack-sdk"] 631 | telegram = ["requests"] 632 | 633 | [[package]] 634 | name = "typing-extensions" 635 | version = "4.12.2" 636 | description = "Backported and Experimental Type Hints for Python 3.8+" 637 | optional = false 638 | python-versions = ">=3.8" 639 | files = [ 640 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 641 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 642 | ] 643 | 644 | [[package]] 645 | name = "virtualenv" 646 | version = "20.29.1" 647 | description = "Virtual Python Environment builder" 648 | optional = true 649 | python-versions = ">=3.8" 650 | files = [ 651 | {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, 652 | {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, 653 | ] 654 | 655 | [package.dependencies] 656 | distlib = ">=0.3.7,<1" 657 | filelock = ">=3.12.2,<4" 658 | platformdirs = ">=3.9.1,<5" 659 | 660 | [package.extras] 661 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 662 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 663 | 664 | [extras] 665 | dev = [] 666 | doc = [] 667 | test = [] 668 | 669 | [metadata] 670 | lock-version = "2.0" 671 | python-versions = "^3.8" 672 | content-hash = "9f31d5ad6e62e19cef6506ed0d1b6a4cf3f1ee8d657d4acd6ac10287614a4545" 673 | -------------------------------------------------------------------------------- /slist/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import concurrent.futures 5 | from dataclasses import dataclass 6 | import itertools 7 | import random 8 | import re 9 | import statistics 10 | import sys 11 | import typing 12 | from collections import OrderedDict 13 | 14 | from functools import reduce 15 | from itertools import tee 16 | from typing import ( 17 | Generic, 18 | TypeVar, 19 | Hashable, 20 | Protocol, 21 | Callable, 22 | Optional, 23 | List, 24 | Union, 25 | Sequence, 26 | overload, 27 | Any, 28 | Tuple, 29 | ) 30 | 31 | # Needed for https://github.com/python/typing_extensions/issues/7 32 | from typing_extensions import NamedTuple 33 | 34 | 35 | A = TypeVar("A") 36 | B = TypeVar("B") 37 | C = TypeVar("C") 38 | D = TypeVar("D") 39 | E = TypeVar("E") 40 | F = TypeVar("F") 41 | 42 | CanCompare = TypeVar("CanCompare", bound="Comparable") 43 | CanHash = TypeVar("CanHash", bound=Hashable) 44 | 45 | 46 | def identity(x: A) -> A: 47 | return x 48 | 49 | 50 | A_co = TypeVar("A_co", covariant=True) 51 | B_co = TypeVar("B_co", covariant=True) 52 | 53 | 54 | class Group(NamedTuple, Generic[A_co, B_co]): 55 | """This is a NamedTuple so that you can easily access the key and values""" 56 | 57 | key: A_co 58 | values: B_co 59 | 60 | def map_key(self, func: Callable[[A_co], C]) -> Group[C, B_co]: 61 | return Group(func(self.key), self.values) 62 | 63 | def map_values(self, func: Callable[[B_co], C]) -> Group[A_co, C]: 64 | return Group(self.key, func(self.values)) 65 | 66 | 67 | class Addable(Protocol): 68 | def __add__(self: A, other: A, /) -> A: ... 69 | 70 | 71 | CanAdd = TypeVar("CanAdd", bound=Addable) 72 | 73 | 74 | @dataclass(frozen=True) 75 | class AverageStats: 76 | average: float 77 | standard_deviation: float 78 | upper_confidence_interval_95: float 79 | lower_confidence_interval_95: float 80 | average_plus_minus_95: float 81 | count: int 82 | 83 | def __str__(self) -> str: 84 | return f"Average: {self.average}, SD: {self.standard_deviation}, 95% CI: ({self.lower_confidence_interval_95}, {self.upper_confidence_interval_95})" 85 | 86 | 87 | class Comparable(Protocol): 88 | def __lt__(self: CanCompare, other: CanCompare, /) -> bool: ... 89 | 90 | def __gt__(self: CanCompare, other: CanCompare, /) -> bool: ... 91 | 92 | def __le__(self: CanCompare, other: CanCompare, /) -> bool: ... 93 | 94 | def __ge__(self: CanCompare, other: CanCompare, /) -> bool: ... 95 | 96 | 97 | class Slist(List[A]): 98 | @staticmethod 99 | def one(element: A) -> Slist[A]: 100 | """Create a new Slist with a single element. 101 | 102 | Parameters 103 | ---------- 104 | element : A 105 | The element to create the list with 106 | 107 | Returns 108 | ------- 109 | Slist[A] 110 | A new Slist containing only the given element 111 | 112 | Examples 113 | -------- 114 | >>> Slist.one(5) 115 | Slist([5]) 116 | """ 117 | return Slist([element]) 118 | 119 | @staticmethod 120 | def one_option(element: Optional[A]) -> Slist[A]: 121 | """Create a Slist with one element if it exists, otherwise empty list. 122 | 123 | Equal to ``Slist.one(element).flatten_option()`` 124 | 125 | Parameters 126 | ---------- 127 | element : Optional[A] 128 | The element to create the list with, if it exists 129 | 130 | Returns 131 | ------- 132 | Slist[A] 133 | A new Slist containing the element if it exists, otherwise empty 134 | 135 | Examples 136 | -------- 137 | >>> Slist.one_option(5) 138 | Slist([5]) 139 | >>> Slist.one_option(None) 140 | Slist([]) 141 | """ 142 | return Slist([element]) if element is not None else Slist() 143 | 144 | def any(self, predicate: Callable[[A], bool]) -> bool: 145 | """Check if any element satisfies the predicate. 146 | 147 | Parameters 148 | ---------- 149 | predicate : Callable[[A], bool] 150 | Function that takes an element and returns True/False 151 | 152 | Returns 153 | ------- 154 | bool 155 | True if any element satisfies the predicate, False otherwise 156 | 157 | Examples 158 | -------- 159 | >>> Slist([1, 2, 3, 4]).any(lambda x: x > 3) 160 | True 161 | >>> Slist([1, 2, 3]).any(lambda x: x > 3) 162 | False 163 | """ 164 | for x in self: 165 | if predicate(x): 166 | return True 167 | return False 168 | 169 | def all(self, predicate: Callable[[A], bool]) -> bool: 170 | """Check if all elements satisfy the predicate. 171 | 172 | Parameters 173 | ---------- 174 | predicate : Callable[[A], bool] 175 | Function that takes an element and returns True/False 176 | 177 | Returns 178 | ------- 179 | bool 180 | True if all elements satisfy the predicate, False otherwise 181 | 182 | Examples 183 | -------- 184 | >>> Slist([2, 4, 6]).all(lambda x: x % 2 == 0) 185 | True 186 | >>> Slist([2, 3, 4]).all(lambda x: x % 2 == 0) 187 | False 188 | """ 189 | for x in self: 190 | if not predicate(x): 191 | return False 192 | return True 193 | 194 | def filter(self, predicate: Callable[[A], bool]) -> Slist[A]: 195 | """Create a new Slist with only elements that satisfy the predicate. 196 | 197 | Parameters 198 | ---------- 199 | predicate : Callable[[A], bool] 200 | Function that takes an element and returns True/False 201 | 202 | Returns 203 | ------- 204 | Slist[A] 205 | A new Slist containing only elements that satisfy the predicate 206 | 207 | Examples 208 | -------- 209 | >>> Slist([1, 2, 3, 4]).filter(lambda x: x % 2 == 0) 210 | Slist([2, 4]) 211 | """ 212 | return Slist(filter(predicate, self)) 213 | 214 | def map(self, func: Callable[[A], B]) -> Slist[B]: 215 | """Transform each element using the given function. 216 | 217 | Parameters 218 | ---------- 219 | func : Callable[[A], B] 220 | Function to apply to each element 221 | 222 | Returns 223 | ------- 224 | Slist[B] 225 | A new Slist with transformed elements 226 | 227 | Examples 228 | -------- 229 | >>> Slist([1, 2, 3]).map(lambda x: x * 2) 230 | Slist([2, 4, 6]) 231 | """ 232 | return Slist(func(item) for item in self) 233 | 234 | @overload 235 | def product(self: Sequence[A], other: Sequence[B], /) -> Slist[Tuple[A, B]]: ... 236 | 237 | @overload 238 | def product(self: Sequence[A], other: Sequence[B], other1: Sequence[C], /) -> Slist[Tuple[A, B, C]]: ... 239 | 240 | @overload 241 | def product( 242 | self: Sequence[A], other: Sequence[B], other1: Sequence[C], other2: Sequence[D], / 243 | ) -> Slist[Tuple[A, B, C, D]]: ... 244 | 245 | @overload 246 | def product( 247 | self: Sequence[A], other: Sequence[B], other1: Sequence[C], other2: Sequence[D], other3: Sequence[E], / 248 | ) -> Slist[Tuple[A, B, C, D, E]]: ... 249 | 250 | @overload 251 | def product( 252 | self: Sequence[A], 253 | other: Sequence[B], 254 | other1: Sequence[C], 255 | other2: Sequence[D], 256 | other3: Sequence[E], 257 | other4: Sequence[F], 258 | /, 259 | ) -> Slist[Tuple[A, B, C, D, E, F]]: ... 260 | 261 | def product(self: Sequence[A], *others: Sequence[Any]) -> Slist[Tuple[Any, ...]]: 262 | """Compute the cartesian product with other sequences. 263 | 264 | Parameters 265 | ---------- 266 | *others : Sequence[Any] 267 | The sequences to compute the product with 268 | 269 | Returns 270 | ------- 271 | Slist[Tuple[Any, ...]] 272 | A new Slist containing tuples of all combinations 273 | 274 | Examples 275 | -------- 276 | >>> Slist([1, 2]).product(['a', 'b']) 277 | Slist([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]) 278 | """ 279 | return Slist(itertools.product(self, *others)) 280 | 281 | def map_2(self: Sequence[Tuple[B, C]], func: Callable[[B, C], D]) -> Slist[D]: 282 | """Map a function over a sequence of 2-tuples. 283 | 284 | Parameters 285 | ---------- 286 | func : Callable[[B, C], D] 287 | Function that takes two arguments and returns a value 288 | 289 | Returns 290 | ------- 291 | Slist[D] 292 | A new Slist with the results of applying func to each tuple 293 | 294 | Examples 295 | -------- 296 | >>> pairs = Slist([(1, 2), (3, 4)]) 297 | >>> pairs.map_2(lambda x, y: x + y) 298 | Slist([3, 7]) 299 | """ 300 | return Slist(func(b, c) for b, c in self) 301 | 302 | def map_enumerate(self, func: Callable[[int, A], B]) -> Slist[B]: 303 | """Map a function over the list with indices. 304 | 305 | Parameters 306 | ---------- 307 | func : Callable[[int, A], B] 308 | Function that takes an index and value and returns a new value 309 | 310 | Returns 311 | ------- 312 | Slist[B] 313 | A new Slist with the results of applying func to each (index, value) pair 314 | 315 | Examples 316 | -------- 317 | >>> Slist(['a', 'b', 'c']).map_enumerate(lambda i, x: f"{i}:{x}") 318 | Slist(['0:a', '1:b', '2:c']) 319 | """ 320 | return Slist(func(idx, item) for idx, item in enumerate(self)) 321 | 322 | def flatten_option(self: Sequence[Optional[B]]) -> Slist[B]: 323 | """Remove None values from a sequence of optional values. 324 | 325 | Parameters 326 | ---------- 327 | self : Sequence[Optional[B]] 328 | A sequence containing optional values 329 | 330 | Returns 331 | ------- 332 | Slist[B] 333 | A new Slist with all non-None values 334 | 335 | Examples 336 | -------- 337 | >>> Slist([1, None, 3, None, 5]).flatten_option() 338 | Slist([1, 3, 5]) 339 | """ 340 | return Slist([item for item in self if item is not None]) 341 | 342 | def flat_map_option(self, func: Callable[[A], Optional[B]]) -> Slist[B]: 343 | """Apply a function that returns optional values and filter out Nones. 344 | 345 | Parameters 346 | ---------- 347 | func : Callable[[A], Optional[B]] 348 | Function that takes a value and returns an optional value 349 | 350 | Returns 351 | ------- 352 | Slist[B] 353 | A new Slist with all non-None results of applying func 354 | 355 | Examples 356 | -------- 357 | >>> def safe_sqrt(x: float) -> Optional[float]: 358 | ... return x ** 0.5 if x >= 0 else None 359 | >>> Slist([4, -1, 9, -4, 16]).flat_map_option(safe_sqrt) 360 | Slist([2.0, 3.0, 4.0]) 361 | """ 362 | return self.map(func).flatten_option() 363 | 364 | def upsample_if(self, predicate: Callable[[A], bool], upsample_by: int) -> Slist[A]: 365 | """Repeat elements that satisfy a predicate. 366 | 367 | Parameters 368 | ---------- 369 | predicate : Callable[[A], bool] 370 | Function that determines which elements to upsample 371 | upsample_by : int 372 | Number of times to repeat each matching element 373 | 374 | Returns 375 | ------- 376 | Slist[A] 377 | A new Slist with matching elements repeated 378 | 379 | Examples 380 | -------- 381 | >>> numbers = Slist([1, 2, 3, 4]) 382 | >>> numbers.upsample_if(lambda x: x % 2 == 0, upsample_by=2) 383 | Slist([1, 2, 2, 3, 4, 4]) 384 | """ 385 | assert upsample_by > 0, "upsample_by must be positive" 386 | new_list = Slist[A]() 387 | for item in self: 388 | if predicate(item): 389 | for _ in range(upsample_by): 390 | new_list.append(item) 391 | else: 392 | new_list.append(item) 393 | return new_list 394 | 395 | def flatten_list(self: Sequence[Sequence[B]]) -> Slist[B]: 396 | """Flatten a sequence of sequences into a single list. 397 | 398 | Parameters 399 | ---------- 400 | self : Sequence[Sequence[B]] 401 | A sequence of sequences to flatten 402 | 403 | Returns 404 | ------- 405 | Slist[B] 406 | A new Slist with all elements from all sequences 407 | 408 | Examples 409 | -------- 410 | >>> nested = Slist([[1, 2], [3, 4], [5, 6]]) 411 | >>> nested.flatten_list() 412 | Slist([1, 2, 3, 4, 5, 6]) 413 | """ 414 | flat_list: Slist[B] = Slist() 415 | for sublist in self: 416 | for item in sublist: 417 | flat_list.append(item) 418 | return flat_list 419 | 420 | def enumerated(self) -> Slist[Tuple[int, A]]: 421 | """Create a list of tuples containing indices and values. 422 | 423 | Returns 424 | ------- 425 | Slist[Tuple[int, A]] 426 | A new Slist of (index, value) tuples 427 | 428 | Examples 429 | -------- 430 | >>> Slist(['a', 'b', 'c']).enumerated() 431 | Slist([(0, 'a'), (1, 'b'), (2, 'c')]) 432 | """ 433 | return Slist(enumerate(self)) 434 | 435 | def shuffle(self, seed: Optional[str] = None) -> Slist[A]: 436 | """Create a new randomly shuffled list. 437 | 438 | Parameters 439 | ---------- 440 | seed : Optional[str], optional 441 | Random seed for reproducibility, by default None 442 | 443 | Returns 444 | ------- 445 | Slist[A] 446 | A new Slist with elements in random order 447 | 448 | Examples 449 | -------- 450 | >>> Slist([1, 2, 3, 4]).shuffle(seed="42") # Reproducible shuffle 451 | Slist([2, 4, 1, 3]) 452 | """ 453 | new = self.copy() 454 | random.Random(seed).shuffle(new) 455 | return Slist(new) 456 | 457 | def choice( 458 | self, 459 | seed: Optional[str] = None, 460 | weights: Optional[List[int]] = None, 461 | ) -> A: 462 | """Randomly select an element from the list. 463 | 464 | Parameters 465 | ---------- 466 | seed : Optional[str], optional 467 | Random seed for reproducibility, by default None 468 | weights : Optional[List[int]], optional 469 | List of weights for weighted random selection, by default None 470 | 471 | Returns 472 | ------- 473 | A 474 | A randomly selected element 475 | 476 | Examples 477 | -------- 478 | >>> Slist([1, 2, 3, 4]).choice(seed="42") # Reproducible choice 479 | 2 480 | >>> Slist([1, 2, 3]).choice(weights=[1, 2, 1]) # Weighted choice 481 | 2 # More likely to select 2 due to higher weight 482 | """ 483 | if weights: 484 | return random.Random(seed).choices(self, weights=weights, k=1)[0] 485 | else: 486 | return random.Random(seed).choice(self) 487 | 488 | def sample(self, n: int, seed: Optional[str] = None) -> Slist[A]: 489 | """Randomly sample n elements from the list without replacement. 490 | 491 | Parameters 492 | ---------- 493 | n : int 494 | Number of elements to sample 495 | seed : Optional[str], optional 496 | Random seed for reproducibility, by default None 497 | 498 | Returns 499 | ------- 500 | Slist[A] 501 | A new Slist with n randomly selected elements 502 | 503 | Examples 504 | -------- 505 | >>> Slist([1, 2, 3, 4, 5]).sample(3, seed="42") 506 | Slist([2, 4, 1]) 507 | """ 508 | return Slist(random.Random(seed).sample(self, n)) 509 | 510 | def for_each(self, func: Callable[[A], None]) -> Slist[A]: 511 | """Apply a side-effect function to each element and return the original list. 512 | 513 | Parameters 514 | ---------- 515 | func : Callable[[A], None] 516 | Function to apply to each element for its side effects 517 | 518 | Returns 519 | ------- 520 | Slist[A] 521 | The original list, unchanged 522 | 523 | Examples 524 | -------- 525 | >>> nums = Slist([1, 2, 3]) 526 | >>> nums.for_each(print) # Prints each number 527 | 1 528 | 2 529 | 3 530 | >>> nums # Original list is unchanged 531 | Slist([1, 2, 3]) 532 | """ 533 | for item in self: 534 | func(item) 535 | return self 536 | 537 | def group_by(self, key: Callable[[A], CanHash]) -> Slist[Group[CanHash, Slist[A]]]: 538 | """Group elements by a key function. 539 | 540 | Parameters 541 | ---------- 542 | key : Callable[[A], CanHash] 543 | Function to compute the group key for each element 544 | 545 | Returns 546 | ------- 547 | Slist[Group[CanHash, Slist[A]]] 548 | A new Slist of Groups, where each Group contains: 549 | - key: The grouping key 550 | - values: Slist of elements in that group 551 | 552 | Examples 553 | -------- 554 | >>> numbers = Slist([1, 2, 3, 4]) 555 | >>> groups = numbers.group_by(lambda x: x % 2) # Group by even/odd 556 | >>> groups.map(lambda g: (g.key, list(g.values))) 557 | Slist([(1, [1, 3]), (0, [2, 4])]) 558 | """ 559 | d: typing.OrderedDict[CanHash, Slist[A]] = OrderedDict() 560 | for elem in self: 561 | k = key(elem) 562 | if k in d: 563 | d[k].append(elem) 564 | else: 565 | d[k] = Slist([elem]) 566 | return Slist(Group(key=key, values=value) for key, value in d.items()) 567 | 568 | @overload 569 | def ungroup(self: Slist[Group[Any, Slist[C]]]) -> Slist[C]: ... 570 | 571 | @overload 572 | def ungroup(self: Slist[Group[Any, Sequence[C]]]) -> Slist[C]: ... 573 | 574 | def ungroup(self: Slist[Group[Any, Slist[C]]] | Slist[Group[Any, Sequence[C]]]) -> Slist[C]: 575 | """Convert a list of groups back into a flat list of values. 576 | 577 | Parameters 578 | ---------- 579 | self : Slist[Group[Any, Slist[C]]] | Slist[Group[Any, Sequence[C]]] 580 | A list of groups to ungroup 581 | 582 | Returns 583 | ------- 584 | Slist[C] 585 | A flat list containing all values from all groups 586 | 587 | Examples 588 | -------- 589 | >>> groups = Slist([Group(0, [1, 2]), Group(1, [3, 4])]) 590 | >>> groups.ungroup() 591 | Slist([1, 2, 3, 4]) 592 | """ 593 | casted: Slist[Group[Any, Slist[C]]] = self # type: ignore 594 | return casted.map_2(lambda _, values: values).flatten_list() 595 | 596 | def map_on_group_values(self: Slist[Group[B, Slist[C]]], func: Callable[[Slist[C]], D]) -> Slist[Group[B, D]]: 597 | """Apply a function to the values of each group. 598 | 599 | Parameters 600 | ---------- 601 | func : Callable[[Slist[C]], D] 602 | Function to apply to each group's values 603 | 604 | Returns 605 | ------- 606 | Slist[Group[B, D]] 607 | A new list of groups with transformed values 608 | 609 | Examples 610 | -------- 611 | >>> groups = Slist([1, 2, 3, 4]).group_by(lambda x: x % 2) 612 | >>> groups.map_on_group_values(lambda values: sum(values)) 613 | Slist([Group(key=1, values=4), Group(key=0, values=6)]) 614 | """ 615 | return self.map(lambda group: group.map_values(func)) 616 | 617 | def map_on_group_values_list( 618 | self: Slist[Group[A, Sequence[B]]], func: Callable[[B], C] 619 | ) -> Slist[Group[A, Sequence[C]]]: 620 | """Apply a function to each element in each group's values. 621 | 622 | Parameters 623 | ---------- 624 | func : Callable[[B], C] 625 | Function to apply to each element in each group's values 626 | 627 | Returns 628 | ------- 629 | Slist[Group[A, Sequence[C]]] 630 | A new list of groups with transformed elements 631 | 632 | Examples 633 | -------- 634 | >>> groups = Slist([Group(1, [1, 2]), Group(2, [3, 4])]) 635 | >>> groups.map_on_group_values_list(lambda x: x * 2) 636 | Slist([Group(1, [2, 4]), Group(2, [6, 8])]) 637 | """ 638 | return self.map(lambda group: group.map_values(lambda values: Slist(values).map(func))) 639 | 640 | def value_counts(self, key: Callable[[A], CanHash], sort: bool = True) -> Slist[Group[CanHash, int]]: 641 | """Count occurrences of each unique value or key-derived value. 642 | 643 | Parameters 644 | ---------- 645 | key : Callable[[A], CanHash] 646 | Function to extract the value to count by 647 | sort : bool, default=True 648 | If True, sorts the results by count in descending order 649 | 650 | Returns 651 | ------- 652 | Slist[Group[CanHash, int]] 653 | A list of groups with keys and their counts 654 | 655 | Examples 656 | -------- 657 | >>> Slist(['apple', 'banana', 'cherry']).value_counts(key=lambda x: x) 658 | Slist([Group(key='apple', values=1), Group(key='banana', values=1), Group(key='cherry', values=1)]) 659 | """ 660 | result = self.group_by(key).map_on_group_values(len) 661 | if sort: 662 | return result.sort_by(key=lambda group: group.values, reverse=True) 663 | return result 664 | 665 | def value_percentage(self, key: Callable[[A], CanHash], sort: bool = True) -> Slist[Group[CanHash, float]]: 666 | """Count occurrences of each unique value or key-derived value. 667 | 668 | Parameters 669 | ---------- 670 | key : Callable[[A], CanHash] 671 | Function to extract the value to count by 672 | sort : bool, default=True 673 | If True, sorts the results by percentage in descending order 674 | 675 | Returns 676 | ------- 677 | Slist[Group[CanHash, float]] 678 | A list of groups with keys and their percentage of total 679 | 680 | Examples 681 | -------- 682 | >>> Slist(['a', 'a', 'b']).value_percentage(key=lambda x: x) 683 | Slist([Group(key='a', values=0.6666666666666666), Group(key='b', values=0.3333333333333333)]) 684 | """ 685 | total = len(self) 686 | if total == 0: 687 | return Slist() 688 | 689 | counts = self.value_counts(key, sort=False) 690 | result = counts.map(lambda group: Group(key=group.key, values=group.values / total)) # type: ignore 691 | 692 | if sort: 693 | return result.sort_by(key=lambda group: group.values, reverse=True) 694 | return result 695 | 696 | def to_dict(self: Sequence[Tuple[CanHash, B]]) -> typing.Dict[CanHash, B]: 697 | """ 698 | Transforms a Slist of key value pairs to a dictionary 699 | >>> Slist([(1, Slist([1, 1])), (2, Slist([2, 2])])).to_dict() 700 | # Equivalent to 701 | >>> Slist([1, 1, 2, 2]).group_by(lambda x: x).to_dict() 702 | {1: Slist([1, 1]), 2: Slist([2, 2])} 703 | """ 704 | return dict(self) 705 | 706 | def to_set(self) -> typing.Set[A]: 707 | """ 708 | Convert the Slist to a set. 709 | """ 710 | return set(self) 711 | 712 | @staticmethod 713 | def from_dict(a_dict: typing.Dict[CanHash, A]) -> Slist[Tuple[CanHash, A]]: 714 | """Convert a dictionary to a Slist of tuple values. 715 | 716 | Parameters 717 | ---------- 718 | a_dict : Dict[CanHash, A] 719 | Dictionary to convert 720 | 721 | Returns 722 | ------- 723 | Slist[Tuple[CanHash, A]] 724 | List of key-value tuples from the dictionary 725 | 726 | Examples 727 | -------- 728 | >>> Slist.from_dict({1: 'a', 2: 'b'}) 729 | Slist([(1, 'a'), (2, 'b')]) 730 | """ 731 | return Slist(tup for tup in a_dict.items()) 732 | 733 | def for_each_enumerate(self, func: Callable[[int, A], None]) -> Slist[A]: 734 | """Apply a side-effect function to each element with its index. 735 | 736 | Parameters 737 | ---------- 738 | func : Callable[[int, A], None] 739 | Function taking an index and value, applied for side effects 740 | 741 | Returns 742 | ------- 743 | Slist[A] 744 | The original list, unchanged 745 | 746 | Examples 747 | -------- 748 | >>> nums = Slist(['a', 'b', 'c']) 749 | >>> nums.for_each_enumerate(lambda i, x: print(f"{i}: {x}")) 750 | 0: a 751 | 1: b 752 | 2: c 753 | >>> nums # Original list is unchanged 754 | Slist(['a', 'b', 'c']) 755 | """ 756 | for idx, item in enumerate(self): 757 | func(idx, item) 758 | return self 759 | 760 | def max_option(self: Sequence[CanCompare]) -> Optional[CanCompare]: 761 | """Get the maximum element if it exists. 762 | 763 | Returns 764 | ------- 765 | Optional[CanCompare] 766 | Maximum element, or None if list is empty 767 | 768 | Examples 769 | -------- 770 | >>> Slist([1, 3, 2]).max_option() 771 | 3 772 | >>> Slist([]).max_option() 773 | None 774 | """ 775 | return max(self) if self else None 776 | 777 | def max_by(self, key: Callable[[A], CanCompare]) -> Optional[A]: 778 | """Get the element with maximum value by key function. 779 | 780 | Parameters 781 | ---------- 782 | key : Callable[[A], CanCompare] 783 | Function to compute comparison value for each element 784 | 785 | Returns 786 | ------- 787 | Optional[A] 788 | Element with maximum key value, or None if list is empty 789 | 790 | Examples 791 | -------- 792 | >>> Slist(['a', 'bbb', 'cc']).max_by(len) 793 | 'bbb' 794 | >>> Slist([]).max_by(len) 795 | None 796 | """ 797 | return max(self, key=key) if self.length > 0 else None 798 | 799 | def max_by_ordering(self, ordering: Callable[[A, A], bool]) -> Optional[A]: 800 | """Get maximum element using custom ordering function. 801 | 802 | Parameters 803 | ---------- 804 | ordering : Callable[[A, A], bool] 805 | Function that returns True if first argument should be considered larger 806 | 807 | Returns 808 | ------- 809 | Optional[A] 810 | Maximum element by ordering, or None if list is empty 811 | 812 | Examples 813 | -------- 814 | >>> # Custom ordering: consider numbers closer to 10 as "larger" 815 | >>> nums = Slist([1, 5, 8, 15]) 816 | >>> nums.max_by_ordering(lambda x, y: abs(x-10) < abs(y-10)) 817 | 8 818 | """ 819 | theMax: Optional[A] = self.first_option 820 | for currentItem in self: 821 | if theMax is not None: 822 | if ordering(theMax, currentItem): 823 | theMax = currentItem 824 | return theMax 825 | 826 | def min_option(self: Sequence[CanCompare]) -> Optional[CanCompare]: 827 | """Get the minimum element if it exists. 828 | 829 | Returns 830 | ------- 831 | Optional[CanCompare] 832 | Minimum element, or None if list is empty 833 | 834 | Examples 835 | -------- 836 | >>> Slist([3, 1, 2]).min_option() 837 | 1 838 | >>> Slist([]).min_option() 839 | None 840 | """ 841 | return min(self) if self else None 842 | 843 | def min_by(self, key: Callable[[A], CanCompare]) -> Optional[A]: 844 | """Get the element with minimum value by key function. 845 | 846 | Parameters 847 | ---------- 848 | key : Callable[[A], CanCompare] 849 | Function to compute comparison value for each element 850 | 851 | Returns 852 | ------- 853 | Optional[A] 854 | Element with minimum key value, or None if list is empty 855 | 856 | Examples 857 | -------- 858 | >>> Slist(['aaa', 'b', 'cc']).min_by(len) 859 | 'b' 860 | >>> Slist([]).min_by(len) 861 | None 862 | """ 863 | return min(self, key=key) if self.length > 0 else None 864 | 865 | def min_by_ordering(self: Slist[CanCompare]) -> Optional[CanCompare]: 866 | """Get minimum element using default ordering. 867 | 868 | Returns 869 | ------- 870 | Optional[CanCompare] 871 | Minimum element, or None if list is empty 872 | 873 | Examples 874 | -------- 875 | >>> Slist([3, 1, 2]).min_by_ordering() 876 | 1 877 | >>> Slist([]).min_by_ordering() 878 | None 879 | """ 880 | return min(self) if self else None 881 | 882 | def get(self, index: int, or_else: B) -> Union[A, B]: 883 | """Get element at index with fallback value. 884 | 885 | Parameters 886 | ---------- 887 | index : int 888 | Index to get element from 889 | or_else : B 890 | Value to return if index is out of bounds 891 | 892 | Returns 893 | ------- 894 | Union[A, B] 895 | Element at index if it exists, otherwise or_else value 896 | 897 | Examples 898 | -------- 899 | >>> Slist([1, 2, 3]).get(1, -1) 900 | 2 901 | >>> Slist([1, 2, 3]).get(5, -1) 902 | -1 903 | """ 904 | try: 905 | return self.__getitem__(index) 906 | except IndexError: 907 | return or_else 908 | 909 | def get_option(self, index: int) -> Optional[A]: 910 | """Get element at index if it exists. 911 | 912 | Parameters 913 | ---------- 914 | index : int 915 | Index to get element from 916 | 917 | Returns 918 | ------- 919 | Optional[A] 920 | Element at index if it exists, otherwise None 921 | 922 | Examples 923 | -------- 924 | >>> Slist([1, 2, 3]).get_option(1) 925 | 2 926 | >>> Slist([1, 2, 3]).get_option(5) 927 | None 928 | """ 929 | try: 930 | return self.__getitem__(index) 931 | except IndexError: 932 | return None 933 | 934 | def pairwise(self) -> Slist[Tuple[A, A]]: 935 | """Return overlapping pairs of consecutive elements. 936 | 937 | Returns 938 | ------- 939 | Slist[Tuple[A, A]] 940 | List of tuples containing consecutive overlapping pairs 941 | 942 | Examples 943 | -------- 944 | >>> Slist([1, 2, 3, 4]).pairwise() 945 | Slist([(1, 2), (2, 3), (3, 4)]) 946 | >>> Slist([1]).pairwise() 947 | Slist([]) 948 | >>> Slist([]).pairwise() 949 | Slist([]) 950 | 951 | Notes 952 | ----- 953 | Inspired by more-itertools pairwise function. Creates an iterator of 954 | overlapping pairs from the input sequence. 955 | """ 956 | a, b = tee(self) 957 | next(b, None) 958 | return Slist(zip(a, b)) 959 | 960 | def print_length(self, printer: Callable[[str], None] = print, prefix: str = "Slist Length: ") -> Slist[A]: 961 | """Print the length of the list and return the original list. 962 | 963 | Parameters 964 | ---------- 965 | printer : Callable[[str], None], optional 966 | Function to print the output, by default print 967 | prefix : str, optional 968 | Prefix string before the length, by default "Slist Length: " 969 | 970 | Returns 971 | ------- 972 | Slist[A] 973 | The original list unchanged 974 | 975 | Examples 976 | -------- 977 | >>> Slist([1,2,3]).print_length() 978 | Slist Length: 3 979 | Slist([1, 2, 3]) 980 | """ 981 | string = f"{prefix}{len(self)}" 982 | printer(string) 983 | return self 984 | 985 | @property 986 | def is_empty(self) -> bool: 987 | """Check if the list is empty. 988 | 989 | Returns 990 | ------- 991 | bool 992 | True if the list has no elements 993 | 994 | Examples 995 | -------- 996 | >>> Slist([]).is_empty 997 | True 998 | >>> Slist([1]).is_empty 999 | False 1000 | """ 1001 | return len(self) == 0 1002 | 1003 | @property 1004 | def not_empty(self) -> bool: 1005 | """Check if the list has any elements. 1006 | 1007 | Returns 1008 | ------- 1009 | bool 1010 | True if the list has at least one element 1011 | 1012 | Examples 1013 | -------- 1014 | >>> Slist([1]).not_empty 1015 | True 1016 | >>> Slist([]).not_empty 1017 | False 1018 | """ 1019 | return len(self) > 0 1020 | 1021 | @property 1022 | def length(self) -> int: 1023 | """Get the number of elements in the list. 1024 | 1025 | Returns 1026 | ------- 1027 | int 1028 | Number of elements 1029 | 1030 | Examples 1031 | -------- 1032 | >>> Slist([1, 2, 3]).length 1033 | 3 1034 | """ 1035 | return len(self) 1036 | 1037 | @property 1038 | def last_option(self) -> Optional[A]: 1039 | """Get the last element if it exists. 1040 | 1041 | Returns 1042 | ------- 1043 | Optional[A] 1044 | Last element, or None if list is empty 1045 | 1046 | Examples 1047 | -------- 1048 | >>> Slist([1, 2, 3]).last_option 1049 | 3 1050 | >>> Slist([]).last_option 1051 | None 1052 | """ 1053 | try: 1054 | return self.__getitem__(-1) 1055 | except IndexError: 1056 | return None 1057 | 1058 | @property 1059 | def first_option(self) -> Optional[A]: 1060 | """Get the first element if it exists. 1061 | 1062 | Returns 1063 | ------- 1064 | Optional[A] 1065 | First element, or None if list is empty 1066 | 1067 | Examples 1068 | -------- 1069 | >>> Slist([1, 2, 3]).first_option 1070 | 1 1071 | >>> Slist([]).first_option 1072 | None 1073 | """ 1074 | try: 1075 | return self.__getitem__(0) 1076 | except IndexError: 1077 | return None 1078 | 1079 | @property 1080 | def mode_option(self) -> Optional[A]: 1081 | """Get the most common element if it exists. 1082 | 1083 | Returns 1084 | ------- 1085 | Optional[A] 1086 | Most frequent element, or None if list is empty or has no unique mode 1087 | 1088 | Examples 1089 | -------- 1090 | >>> Slist([1, 2, 2, 3]).mode_option 1091 | 2 1092 | >>> Slist([1, 1, 2, 2]).mode_option # No unique mode 1093 | None 1094 | >>> Slist([]).mode_option 1095 | None 1096 | """ 1097 | try: 1098 | return statistics.mode(self) 1099 | except statistics.StatisticsError: 1100 | return None 1101 | 1102 | def mode_or_raise(self, exception: Exception = RuntimeError("List is empty")) -> A: 1103 | """Get the most common element or raise an exception. 1104 | 1105 | Parameters 1106 | ---------- 1107 | exception : Exception, optional 1108 | Exception to raise if no mode exists, by default RuntimeError("List is empty") 1109 | 1110 | Returns 1111 | ------- 1112 | A 1113 | Most frequent element 1114 | 1115 | Raises 1116 | ------ 1117 | Exception 1118 | If list is empty or has no unique mode 1119 | 1120 | Examples 1121 | -------- 1122 | >>> Slist([1, 2, 2, 3]).mode_or_raise() 1123 | 2 1124 | >>> try: 1125 | ... Slist([]).mode_or_raise() 1126 | ... except RuntimeError as e: 1127 | ... print(str(e)) 1128 | List is empty 1129 | """ 1130 | try: 1131 | return statistics.mode(self) 1132 | except statistics.StatisticsError: 1133 | raise exception 1134 | 1135 | def first_or_raise(self, exception: Exception = RuntimeError("List is empty")) -> A: 1136 | """Get the first element or raise an exception. 1137 | 1138 | Parameters 1139 | ---------- 1140 | exception : Exception, optional 1141 | Exception to raise if list is empty, by default RuntimeError("List is empty") 1142 | 1143 | Returns 1144 | ------- 1145 | A 1146 | First element 1147 | 1148 | Raises 1149 | ------ 1150 | Exception 1151 | If list is empty 1152 | 1153 | Examples 1154 | -------- 1155 | >>> Slist([1, 2, 3]).first_or_raise() 1156 | 1 1157 | >>> try: 1158 | ... Slist([]).first_or_raise() 1159 | ... except RuntimeError as e: 1160 | ... print(str(e)) 1161 | List is empty 1162 | """ 1163 | try: 1164 | return self.__getitem__(0) 1165 | except IndexError: 1166 | raise exception 1167 | 1168 | def last_or_raise(self, exception: Exception = RuntimeError("List is empty")) -> A: 1169 | """Get the last element or raise an exception. 1170 | 1171 | Parameters 1172 | ---------- 1173 | exception : Exception, optional 1174 | Exception to raise if list is empty, by default RuntimeError("List is empty") 1175 | 1176 | Returns 1177 | ------- 1178 | A 1179 | Last element 1180 | 1181 | Raises 1182 | ------ 1183 | Exception 1184 | If list is empty 1185 | 1186 | Examples 1187 | -------- 1188 | >>> Slist([1, 2, 3]).last_or_raise() 1189 | 3 1190 | >>> try: 1191 | ... Slist([]).last_or_raise() 1192 | ... except RuntimeError as e: 1193 | ... print(str(e)) 1194 | List is empty 1195 | """ 1196 | try: 1197 | return self.__getitem__(-1) 1198 | except IndexError: 1199 | raise exception 1200 | 1201 | def find_one(self, predicate: Callable[[A], bool]) -> Optional[A]: 1202 | """Find first element that satisfies a predicate. 1203 | 1204 | Parameters 1205 | ---------- 1206 | predicate : Callable[[A], bool] 1207 | Function that returns True for the desired element 1208 | 1209 | Returns 1210 | ------- 1211 | Optional[A] 1212 | First matching element, or None if no match found 1213 | 1214 | Examples 1215 | -------- 1216 | >>> Slist([1, 2, 3, 4]).find_one(lambda x: x > 2) 1217 | 3 1218 | >>> Slist([1, 2, 3]).find_one(lambda x: x > 5) 1219 | None 1220 | """ 1221 | for item in self: 1222 | if predicate(item): 1223 | return item 1224 | return None 1225 | 1226 | def find_one_idx(self, predicate: Callable[[A], bool]) -> Optional[int]: 1227 | """Find index of first element that satisfies a predicate. 1228 | 1229 | Parameters 1230 | ---------- 1231 | predicate : Callable[[A], bool] 1232 | Function that returns True for the desired element 1233 | 1234 | Returns 1235 | ------- 1236 | Optional[int] 1237 | Index of first matching element, or None if no match found 1238 | 1239 | Examples 1240 | -------- 1241 | >>> Slist([1, 2, 3, 4]).find_one_idx(lambda x: x > 2) 1242 | 2 1243 | >>> Slist([1, 2, 3]).find_one_idx(lambda x: x > 5) 1244 | None 1245 | """ 1246 | for idx, item in enumerate(self): 1247 | if predicate(item): 1248 | return idx 1249 | return None 1250 | 1251 | def find_last_idx(self, predicate: Callable[[A], bool]) -> Optional[int]: 1252 | """Find index of last element that satisfies a predicate. 1253 | 1254 | Parameters 1255 | ---------- 1256 | predicate : Callable[[A], bool] 1257 | Function that returns True for the desired element 1258 | 1259 | Returns 1260 | ------- 1261 | Optional[int] 1262 | Index of last matching element, or None if no match found 1263 | 1264 | Examples 1265 | -------- 1266 | >>> Slist([1, 2, 3, 2, 1]).find_last_idx(lambda x: x == 2) 1267 | 3 1268 | >>> Slist([1, 2, 3]).find_last_idx(lambda x: x > 5) 1269 | None 1270 | """ 1271 | indexes = [] 1272 | for idx, item in enumerate(self): 1273 | if predicate(item): 1274 | indexes.append(idx) 1275 | return indexes[-1] if indexes else None 1276 | 1277 | def find_one_idx_or_raise( 1278 | self, 1279 | predicate: Callable[[A], bool], 1280 | exception: Exception = RuntimeError("Failed to find predicate"), 1281 | ) -> int: 1282 | """Find index of first element that satisfies a predicate or raise exception. 1283 | 1284 | Parameters 1285 | ---------- 1286 | predicate : Callable[[A], bool] 1287 | Function that returns True for the desired element 1288 | exception : Exception, optional 1289 | Exception to raise if no match found, by default RuntimeError("Failed to find predicate") 1290 | 1291 | Returns 1292 | ------- 1293 | int 1294 | Index of first matching element 1295 | 1296 | Raises 1297 | ------ 1298 | Exception 1299 | If no matching element is found 1300 | 1301 | Examples 1302 | -------- 1303 | >>> Slist([1, 2, 3, 4]).find_one_idx_or_raise(lambda x: x > 2) 1304 | 2 1305 | >>> try: 1306 | ... Slist([1, 2, 3]).find_one_idx_or_raise(lambda x: x > 5) 1307 | ... except RuntimeError as e: 1308 | ... print(str(e)) 1309 | Failed to find predicate 1310 | """ 1311 | result = self.find_one_idx(predicate=predicate) 1312 | if result is not None: 1313 | return result 1314 | else: 1315 | raise exception 1316 | 1317 | def find_last_idx_or_raise( 1318 | self, 1319 | predicate: Callable[[A], bool], 1320 | exception: Exception = RuntimeError("Failed to find predicate"), 1321 | ) -> int: 1322 | """Find index of last element that satisfies a predicate or raise exception. 1323 | 1324 | Parameters 1325 | ---------- 1326 | predicate : Callable[[A], bool] 1327 | Function that returns True for the desired element 1328 | exception : Exception, optional 1329 | Exception to raise if no match found, by default RuntimeError("Failed to find predicate") 1330 | 1331 | Returns 1332 | ------- 1333 | int 1334 | Index of last matching element 1335 | 1336 | Raises 1337 | ------ 1338 | Exception 1339 | If no matching element is found 1340 | 1341 | Examples 1342 | -------- 1343 | >>> Slist([1, 2, 3, 2, 1]).find_last_idx_or_raise(lambda x: x == 2) 1344 | 3 1345 | >>> try: 1346 | ... Slist([1, 2, 3]).find_last_idx_or_raise(lambda x: x > 5) 1347 | ... except RuntimeError as e: 1348 | ... print(str(e)) 1349 | Failed to find predicate 1350 | """ 1351 | result = self.find_last_idx(predicate=predicate) 1352 | if result is not None: 1353 | return result 1354 | else: 1355 | raise exception 1356 | 1357 | def take(self, n: int) -> Slist[A]: 1358 | return Slist(self[:n]) 1359 | 1360 | def take_or_raise(self, n: int) -> Slist[A]: 1361 | # raises if we end up having less elements than n 1362 | if len(self) < n: 1363 | raise ValueError(f"Cannot take {n} elements from a list of length {len(self)}") 1364 | return Slist(self[:n]) 1365 | 1366 | def take_until_exclusive(self, predicate: Callable[[A], bool]) -> Slist[A]: 1367 | """Takes the first elements until the predicate is true. 1368 | Does not include the element that caused the predicate to return true.""" 1369 | new: Slist[A] = Slist() 1370 | for x in self: 1371 | if predicate(x): 1372 | break 1373 | else: 1374 | new.append(x) 1375 | return new 1376 | 1377 | def take_until_inclusive(self, predicate: Callable[[A], bool]) -> Slist[A]: 1378 | """Takes the first elements until the predicate is true. 1379 | Includes the element that caused the predicate to return true.""" 1380 | new: Slist[A] = Slist() 1381 | for x in self: 1382 | if predicate(x): 1383 | new.append(x) 1384 | break 1385 | else: 1386 | new.append(x) 1387 | return new 1388 | 1389 | def sort_by(self, key: Callable[[A], CanCompare], reverse: bool = False) -> Slist[A]: 1390 | new = self.copy() 1391 | return Slist(sorted(new, key=key, reverse=reverse)) 1392 | 1393 | def percentile_by(self, key: Callable[[A], CanCompare], percentile: float) -> A: 1394 | """Gets the element at the given percentile""" 1395 | if percentile < 0 or percentile > 1: 1396 | raise ValueError(f"Percentile must be between 0 and 1. Got {percentile}") 1397 | if self.length == 0: 1398 | raise ValueError("Cannot get percentile of empty list") 1399 | result = self.sort_by(key).get(int(len(self) * percentile), None) 1400 | assert result is not None 1401 | return result 1402 | 1403 | def median_by(self, key: Callable[[A], CanCompare]) -> A: 1404 | """Gets the median element""" 1405 | if self.length == 0: 1406 | raise ValueError("Cannot get median of empty list") 1407 | return self.percentile_by(key, 0.5) 1408 | 1409 | def sorted(self: Slist[CanCompare], reverse: bool = False) -> Slist[CanCompare]: 1410 | return self.sort_by(key=identity, reverse=reverse) 1411 | 1412 | def reversed(self) -> Slist[A]: 1413 | """Returns a new list with the elements in reversed order""" 1414 | return Slist(reversed(self)) 1415 | 1416 | def sort_by_penalise_duplicates( 1417 | self, 1418 | sort_key: Callable[[A], CanCompare], 1419 | duplicate_key: Callable[[A], CanHash], 1420 | reverse: bool = False, 1421 | ) -> Slist[A]: 1422 | """Sort on a given sort key, but penalises duplicate_key such that they will be at the back of the list 1423 | # >>> Slist([6, 5, 4, 3, 2, 1, 1, 1]).sort_by_penalise_duplicates(sort_key=identity, duplicate_key=identity) 1424 | [1, 2, 3, 4, 5, 6, 1, 1] 1425 | """ 1426 | non_dupes = Slist[A]() 1427 | dupes = Slist[A]() 1428 | 1429 | dupes_tracker: set[CanHash] = set() 1430 | for item in self: 1431 | dupe_key = duplicate_key(item) 1432 | if dupe_key in dupes_tracker: 1433 | dupes.append(item) 1434 | else: 1435 | non_dupes.append(item) 1436 | dupes_tracker.add(dupe_key) 1437 | 1438 | return non_dupes.sort_by(key=sort_key, reverse=reverse) + dupes.sort_by(key=sort_key, reverse=reverse) 1439 | 1440 | def shuffle_with_penalise_duplicates( 1441 | self, 1442 | duplicate_key: Callable[[A], CanHash], 1443 | seed: Optional[str] = None, 1444 | ) -> Slist[A]: 1445 | """Shuffle, but penalises duplicate_key such that they will be at the back of the list 1446 | # >>> Slist([6, 5, 4, 3, 2, 2, 1, 1, 1]).shuffle_by_penalise_duplicates(duplicate_key=identity) 1447 | [6, 4, 1, 3, 5, 2, 1, 2, 1] 1448 | """ 1449 | non_dupes = Slist[A]() 1450 | dupes = Slist[A]() 1451 | shuffled = self.shuffle(seed) 1452 | 1453 | dupes_tracker: set[CanHash] = set() 1454 | for item in shuffled: 1455 | dupe_key = duplicate_key(item) 1456 | if dupe_key in dupes_tracker: 1457 | dupes.append(item) 1458 | else: 1459 | non_dupes.append(item) 1460 | dupes_tracker.add(dupe_key) 1461 | 1462 | return non_dupes.shuffle(seed) + dupes.shuffle(seed) 1463 | 1464 | def __add__(self, other: Sequence[B]) -> Slist[Union[A, B]]: # type: ignore 1465 | return Slist(super().__add__(other)) # type: ignore 1466 | 1467 | def add(self, other: Sequence[B]) -> Slist[Union[A, B]]: 1468 | return self + other 1469 | 1470 | def add_one(self, other: B) -> Slist[Union[A, B]]: 1471 | new: Slist[Union[A, B]] = self.copy() # type: ignore 1472 | new.append(other) 1473 | return new 1474 | 1475 | @overload # type: ignore 1476 | def __getitem__(self, i: int) -> A: 1477 | pass 1478 | 1479 | @overload 1480 | def __getitem__(self, i: slice) -> Slist[A]: 1481 | pass 1482 | 1483 | def __getitem__(self, i: Union[int, slice]) -> Union[A, Slist[A]]: # type: ignore 1484 | if isinstance(i, int): 1485 | return super().__getitem__(i) 1486 | else: 1487 | return Slist(super(Slist, self).__getitem__(i)) 1488 | 1489 | def grouped(self, size: int) -> Slist[Slist[A]]: 1490 | """Groups the list into chunks of size `size`""" 1491 | output: Slist[Slist[A]] = Slist() 1492 | for i in range(0, self.length, size): 1493 | output.append(self[i : i + size]) 1494 | return output 1495 | 1496 | def window(self, size: int) -> Slist[Slist[A]]: 1497 | """Returns a list of windows of size `size` 1498 | If the list is too small or empty, returns an empty list 1499 | Example: 1500 | >>> Slist([1, 2, 3, 4, 5]).window(3) 1501 | [[1, 2, 3], [2, 3, 4], [3, 4, 5]] 1502 | 1503 | >>> Slist([1]).window(2) 1504 | [] 1505 | """ 1506 | output: Slist[Slist[A]] = Slist() 1507 | for i in range(0, self.length - size + 1): 1508 | output.append(self[i : i + size]) 1509 | return output 1510 | 1511 | def distinct(self: Sequence[CanHash]) -> Slist[CanHash]: 1512 | """Remove duplicate elements while preserving order. 1513 | 1514 | Returns 1515 | ------- 1516 | Slist[CanHash] 1517 | A new list with duplicates removed, maintaining original order 1518 | 1519 | Examples 1520 | -------- 1521 | >>> Slist([1, 2, 2, 3, 1, 4]).distinct() 1522 | Slist([1, 2, 3, 4]) 1523 | """ 1524 | seen = set() 1525 | output = Slist[CanHash]() 1526 | for item in self: 1527 | if item in seen: 1528 | continue 1529 | else: 1530 | seen.add(item) 1531 | output.append(item) 1532 | return output 1533 | 1534 | def distinct_by(self, key: Callable[[A], CanHash]) -> Slist[A]: 1535 | """Remove duplicates based on a key function while preserving order. 1536 | 1537 | Parameters 1538 | ---------- 1539 | key : Callable[[A], CanHash] 1540 | Function to compute the unique key for each element 1541 | 1542 | Returns 1543 | ------- 1544 | Slist[A] 1545 | A new list with duplicates removed, maintaining original order 1546 | 1547 | Examples 1548 | -------- 1549 | >>> data = Slist([(1, 'a'), (2, 'b'), (1, 'c')]) 1550 | >>> data.distinct_by(lambda x: x[0]) # Distinct by first element 1551 | Slist([(1, 'a'), (2, 'b')]) 1552 | """ 1553 | seen = set() 1554 | output = Slist[A]() 1555 | for item in self: 1556 | item_hash = key(item) 1557 | if item_hash in seen: 1558 | continue 1559 | else: 1560 | seen.add(item_hash) 1561 | output.append(item) 1562 | return output 1563 | 1564 | def distinct_item_or_raise(self, key: Callable[[A], CanHash]) -> A: 1565 | """Get the single unique item by a key function. 1566 | 1567 | Raises ValueError if the list is empty or contains multiple distinct items. 1568 | 1569 | Parameters 1570 | ---------- 1571 | key : Callable[[A], CanHash] 1572 | Function to compute the unique key for each element 1573 | 1574 | Returns 1575 | ------- 1576 | A 1577 | The single unique item 1578 | 1579 | Raises 1580 | ------ 1581 | ValueError 1582 | If the list is empty or contains multiple distinct items 1583 | 1584 | Examples 1585 | -------- 1586 | >>> Slist([1, 1, 1]).distinct_item_or_raise(lambda x: x) 1587 | 1 1588 | >>> try: 1589 | ... Slist([1, 2, 1]).distinct_item_or_raise(lambda x: x) 1590 | ... except ValueError as e: 1591 | ... print(str(e)) 1592 | Slist is not distinct [1, 2, 1] 1593 | """ 1594 | if not self: 1595 | raise ValueError("Slist is empty") 1596 | distinct = self.distinct_by(key) 1597 | if len(distinct) != 1: 1598 | raise ValueError(f"Slist is not distinct {self}") 1599 | return distinct[0] 1600 | 1601 | def par_map(self, func: Callable[[A], B], executor: concurrent.futures.Executor) -> Slist[B]: 1602 | """Apply a function to each element in parallel using an executor. 1603 | 1604 | Parameters 1605 | ---------- 1606 | func : Callable[[A], B] 1607 | Function to apply to each element. Must be picklable if using ProcessPoolExecutor 1608 | executor : concurrent.futures.Executor 1609 | The executor to use for parallel execution 1610 | 1611 | Returns 1612 | ------- 1613 | Slist[B] 1614 | A new list with the results of applying func to each element 1615 | 1616 | Examples 1617 | -------- 1618 | >>> from concurrent.futures import ThreadPoolExecutor 1619 | >>> with ThreadPoolExecutor() as exe: 1620 | ... Slist([1, 2, 3]).par_map(lambda x: x * 2, exe) 1621 | Slist([2, 4, 6]) 1622 | 1623 | Notes 1624 | ----- 1625 | If using ProcessPoolExecutor, the function must be picklable (e.g., no lambda functions) 1626 | """ 1627 | futures: List[concurrent.futures._base.Future[B]] = [executor.submit(func, item) for item in self] 1628 | results = [] 1629 | for fut in futures: 1630 | results.append(fut.result()) 1631 | return Slist(results) 1632 | 1633 | async def par_map_async( 1634 | self, func: Callable[[A], typing.Awaitable[B]], max_par: int | None = None, tqdm: bool = False 1635 | ) -> Slist[B]: 1636 | """Asynchronously apply a function to each element with optional parallelism limit. 1637 | 1638 | Parameters 1639 | ---------- 1640 | func : Callable[[A], Awaitable[B]] 1641 | Async function to apply to each element 1642 | max_par : int | None, optional 1643 | Maximum number of parallel operations, by default None 1644 | tqdm : bool, optional 1645 | Whether to show a progress bar, by default False 1646 | 1647 | Returns 1648 | ------- 1649 | Slist[B] 1650 | A new Slist with the transformed elements 1651 | 1652 | Examples 1653 | -------- 1654 | >>> async def slow_double(x): 1655 | ... await asyncio.sleep(0.1) 1656 | ... return x * 2 1657 | >>> await Slist([1, 2, 3]).par_map_async(slow_double, max_par=2) 1658 | Slist([2, 4, 6]) 1659 | """ 1660 | if max_par is None: 1661 | if tqdm: 1662 | import tqdm as tqdm_module 1663 | 1664 | tqdm_counter = tqdm_module.tqdm(total=len(self)) 1665 | 1666 | async def func_with_tqdm(item: A) -> B: 1667 | result = await func(item) 1668 | tqdm_counter.update(1) 1669 | return result 1670 | 1671 | return Slist(await asyncio.gather(*[func_with_tqdm(item) for item in self])) 1672 | else: 1673 | # todo: clean up branching 1674 | return Slist(await asyncio.gather(*[func(item) for item in self])) 1675 | 1676 | else: 1677 | assert max_par > 0, "max_par must be greater than 0" 1678 | sema = asyncio.Semaphore(max_par) 1679 | if tqdm: 1680 | import tqdm as tqdm_module 1681 | 1682 | tqdm_counter = tqdm_module.tqdm(total=len(self)) 1683 | 1684 | async def func_with_semaphore(item: A) -> B: 1685 | async with sema: 1686 | result = await func(item) 1687 | tqdm_counter.update(1) 1688 | return result 1689 | 1690 | else: 1691 | 1692 | async def func_with_semaphore(item: A) -> B: 1693 | async with sema: 1694 | return await func(item) 1695 | 1696 | return Slist(await asyncio.gather(*[func_with_semaphore(item) for item in self])) 1697 | 1698 | async def gather(self: Sequence[typing.Awaitable[B]]) -> Slist[B]: 1699 | """Gather and await all awaitables in the sequence. 1700 | 1701 | Returns 1702 | ------- 1703 | Slist[B] 1704 | A new Slist containing the awaited results 1705 | 1706 | Examples 1707 | -------- 1708 | >>> async def slow_value(x): 1709 | ... await asyncio.sleep(0.1) 1710 | ... return x 1711 | >>> awaitables = [slow_value(1), slow_value(2), slow_value(3)] 1712 | >>> await Slist(awaitables).gather() 1713 | Slist([1, 2, 3]) 1714 | """ 1715 | return Slist(await asyncio.gather(*self)) 1716 | 1717 | def filter_text_search(self, key: Callable[[A], str], search: List[str]) -> Slist[A]: 1718 | """Filter items based on text search terms. 1719 | 1720 | Parameters 1721 | ---------- 1722 | key : Callable[[A], str] 1723 | Function to extract searchable text from each item 1724 | search : List[str] 1725 | List of search terms to match (case-insensitive) 1726 | 1727 | Returns 1728 | ------- 1729 | Slist[A] 1730 | Items where key text matches any search term 1731 | 1732 | Examples 1733 | -------- 1734 | >>> items = Slist(['apple pie', 'banana bread', 'cherry cake']) 1735 | >>> items.filter_text_search(lambda x: x, ['pie', 'cake']) 1736 | Slist(['apple pie', 'cherry cake']) 1737 | """ 1738 | 1739 | def matches_search(text: str) -> bool: 1740 | if search: 1741 | search_regex = re.compile("|".join(search), re.IGNORECASE) 1742 | return bool(re.search(search_regex, text)) 1743 | else: 1744 | return True # No filter if search undefined 1745 | 1746 | return self.filter(predicate=lambda item: matches_search(key(item))) 1747 | 1748 | def mk_string(self: Sequence[str], sep: str) -> str: 1749 | """Join string elements with a separator. 1750 | 1751 | Parameters 1752 | ---------- 1753 | sep : str 1754 | Separator to use between elements 1755 | 1756 | Returns 1757 | ------- 1758 | str 1759 | Joined string 1760 | 1761 | Examples 1762 | -------- 1763 | >>> Slist(['a', 'b', 'c']).mk_string(', ') 1764 | 'a, b, c' 1765 | """ 1766 | return sep.join(self) 1767 | 1768 | @overload 1769 | def sum(self: Sequence[int]) -> int: ... 1770 | 1771 | @overload 1772 | def sum(self: Sequence[float]) -> float: ... 1773 | 1774 | def sum( 1775 | self: Sequence[Union[int, float, bool]], 1776 | ) -> Union[int, float]: 1777 | """Returns 0 when the list is empty""" 1778 | return sum(self) 1779 | 1780 | def average( 1781 | self: Sequence[Union[int, float, bool]], 1782 | ) -> Optional[float]: 1783 | """Calculate the arithmetic mean of numeric values. 1784 | 1785 | Returns 1786 | ------- 1787 | Optional[float] 1788 | The average of all values, or None if the list is empty 1789 | 1790 | Examples 1791 | -------- 1792 | >>> Slist([1, 2, 3, 4]).average() 1793 | 2.5 1794 | >>> Slist([]).average() 1795 | None 1796 | """ 1797 | this = typing.cast(Slist[Union[int, float, bool]], self) 1798 | return this.sum() / this.length if this.length > 0 else None 1799 | 1800 | def average_or_raise( 1801 | self: Sequence[Union[int, float, bool]], 1802 | ) -> float: 1803 | """Calculate the arithmetic mean of numeric values. 1804 | 1805 | Returns 1806 | ------- 1807 | float 1808 | The average of all values 1809 | 1810 | Raises 1811 | ------ 1812 | ValueError 1813 | If the list is empty 1814 | 1815 | Examples 1816 | -------- 1817 | >>> Slist([1, 2, 3, 4]).average_or_raise() 1818 | 2.5 1819 | >>> try: 1820 | ... Slist([]).average_or_raise() 1821 | ... except ValueError as e: 1822 | ... print(str(e)) 1823 | Cannot get average of empty list 1824 | """ 1825 | this = typing.cast(Slist[Union[int, float, bool]], self) 1826 | if this.length == 0: 1827 | raise ValueError("Cannot get average of empty list") 1828 | return this.sum() / this.length 1829 | 1830 | def statistics_or_raise( 1831 | self: Sequence[Union[int, float, bool]], 1832 | ) -> AverageStats: 1833 | """Calculate comprehensive statistics for numeric values. 1834 | 1835 | Returns 1836 | ------- 1837 | AverageStats 1838 | Statistics including mean, standard deviation, and confidence intervals 1839 | 1840 | Raises 1841 | ------ 1842 | ValueError 1843 | If the list is empty 1844 | 1845 | Examples 1846 | -------- 1847 | >>> stats = Slist([1, 2, 3, 4, 5]).statistics_or_raise() 1848 | >>> round(stats.average, 2) 1849 | 3.0 1850 | >>> round(stats.standard_deviation, 2) 1851 | 1.58 1852 | """ 1853 | this = typing.cast(Slist[Union[int, float, bool]], self) 1854 | if this.length == 0: 1855 | raise ValueError("Cannot get average of empty list") 1856 | average = this.average_or_raise() 1857 | standard_deviation = this.standard_deviation() 1858 | assert standard_deviation is not None 1859 | standard_error = standard_deviation / ((this.length) ** 0.5) 1860 | upper_ci = average + 1.96 * standard_error 1861 | lower_ci = average - 1.96 * standard_error 1862 | average_plus_minus_95 = 1.96 * standard_error 1863 | return AverageStats( 1864 | average=average, 1865 | standard_deviation=standard_deviation, 1866 | upper_confidence_interval_95=upper_ci, 1867 | lower_confidence_interval_95=lower_ci, 1868 | count=this.length, 1869 | average_plus_minus_95=average_plus_minus_95, 1870 | ) 1871 | 1872 | def standard_deviation(self: Slist[Union[int, float]]) -> Optional[float]: 1873 | """Calculate the population standard deviation. 1874 | 1875 | Returns 1876 | ------- 1877 | Optional[float] 1878 | The standard deviation, or None if the list is empty 1879 | 1880 | Examples 1881 | -------- 1882 | >>> round(Slist([1, 2, 3, 4, 5]).standard_deviation(), 2) 1883 | 1.58 1884 | >>> Slist([]).standard_deviation() 1885 | None 1886 | """ 1887 | return statistics.stdev(self) if self.length > 0 else None 1888 | 1889 | def standardize(self: Slist[Union[int, float]]) -> Slist[float]: 1890 | """Standardize values to have mean 0 and standard deviation 1. 1891 | 1892 | Returns 1893 | ------- 1894 | Slist[float] 1895 | Standardized values, or empty list if input is empty 1896 | 1897 | Examples 1898 | -------- 1899 | >>> result = Slist([1, 2, 3, 4, 5]).standardize() 1900 | >>> [round(x, 2) for x in result] # Rounded for display 1901 | [-1.26, -0.63, 0.0, 0.63, 1.26] 1902 | """ 1903 | mean = self.average() 1904 | sd = self.standard_deviation() 1905 | return Slist((x - mean) / sd for x in self) if mean is not None and sd is not None else Slist() 1906 | 1907 | def fold_left(self, acc: B, func: Callable[[B, A], B]) -> B: 1908 | """Fold left operation (reduce) with initial accumulator. 1909 | 1910 | Parameters 1911 | ---------- 1912 | acc : B 1913 | Initial accumulator value 1914 | func : Callable[[B, A], B] 1915 | Function to combine accumulator with each element 1916 | 1917 | Returns 1918 | ------- 1919 | B 1920 | Final accumulated value 1921 | 1922 | Examples 1923 | -------- 1924 | >>> Slist([1, 2, 3, 4]).fold_left(0, lambda acc, x: acc + x) 1925 | 10 1926 | >>> Slist(['a', 'b', 'c']).fold_left('', lambda acc, x: acc + x) 1927 | 'abc' 1928 | """ 1929 | return reduce(func, self, acc) 1930 | 1931 | def fold_right(self, acc: B, func: Callable[[A, B], B]) -> B: 1932 | """Fold right operation with initial accumulator. 1933 | 1934 | Parameters 1935 | ---------- 1936 | acc : B 1937 | Initial accumulator value 1938 | func : Callable[[A, B], B] 1939 | Function to combine each element with accumulator 1940 | 1941 | Returns 1942 | ------- 1943 | B 1944 | Final accumulated value 1945 | 1946 | Examples 1947 | -------- 1948 | >>> Slist([1, 2, 3]).fold_right('', lambda x, acc: str(x) + acc) 1949 | '321' 1950 | """ 1951 | return reduce(lambda a, b: func(b, a), reversed(self), acc) 1952 | 1953 | def sum_option(self: Sequence[CanAdd]) -> Optional[CanAdd]: 1954 | """Sums the elements of the sequence. Returns None if the sequence is empty. 1955 | 1956 | Returns 1957 | ------- 1958 | Optional[CanAdd] 1959 | The sum of all elements in the sequence, or None if the sequence is empty 1960 | 1961 | Examples 1962 | -------- 1963 | >>> Slist([1, 2, 3]).sum_option() 1964 | 6 1965 | >>> Slist([]).sum_option() 1966 | None 1967 | """ 1968 | return reduce(lambda a, b: a + b, self) if len(self) > 0 else None 1969 | 1970 | def sum_or_raise(self: Sequence[CanAdd]) -> CanAdd: 1971 | """Sums the elements of the sequence. Raises an error if the sequence is empty. 1972 | 1973 | Returns 1974 | ------- 1975 | CanAdd 1976 | The sum of all elements in the sequence 1977 | 1978 | Raises 1979 | ------ 1980 | AssertionError 1981 | If the sequence is empty 1982 | 1983 | Examples 1984 | -------- 1985 | >>> Slist([1, 2, 3]).sum_or_raise() 1986 | 6 1987 | >>> Slist([]).sum_or_raise() # doctest: +IGNORE_EXCEPTION_DETAIL 1988 | Traceback (most recent call last): 1989 | AssertionError: Cannot fold empty list 1990 | """ 1991 | assert len(self) > 0, "Cannot fold empty list" 1992 | return reduce(lambda a, b: a + b, self) 1993 | 1994 | def split_by(self, predicate: Callable[[A], bool]) -> Tuple[Slist[A], Slist[A]]: 1995 | """Split list into two lists based on a predicate. Left list contains items that match the predicate. 1996 | 1997 | Parameters 1998 | ---------- 1999 | predicate : Callable[[A], bool] 2000 | Function to determine which list each element goes into 2001 | 2002 | Returns 2003 | ------- 2004 | Tuple[Slist[A], Slist[A]] 2005 | Tuple of (matching elements, non-matching elements) 2006 | 2007 | Examples 2008 | -------- 2009 | >>> evens, odds = Slist([1, 2, 3, 4, 5]).split_by(lambda x: x % 2 == 0) 2010 | >>> evens 2011 | Slist([2, 4]) 2012 | >>> odds 2013 | Slist([1, 3, 5]) 2014 | """ 2015 | left = Slist[A]() 2016 | right = Slist[A]() 2017 | for item in self: 2018 | if predicate(item): 2019 | left.append(item) 2020 | else: 2021 | right.append(item) 2022 | return left, right 2023 | 2024 | def split_on(self, predicate: Callable[[A], bool]) -> Slist[Slist[A]]: 2025 | """Split list into sublists based on a predicate. 2026 | 2027 | Returns 2028 | ------- 2029 | Slist[Slist[A]] 2030 | List of sublists 2031 | 2032 | Examples 2033 | -------- 2034 | >>> Slist([1, 2, 3, 4, 5]).split_on(lambda x: x % 2 == 0) 2035 | Slist([Slist([1, 3, 5]), Slist([2, 4])]) 2036 | """ 2037 | output: Slist[Slist[A]] = Slist() 2038 | current = Slist[A]() 2039 | for item in self: 2040 | if predicate(item): 2041 | output.append(current) 2042 | current = Slist[A]() 2043 | else: 2044 | current.append(item) 2045 | output.append(current) 2046 | return output 2047 | 2048 | def split_proportion(self, left_proportion: float) -> Tuple[Slist[A], Slist[A]]: 2049 | """Split list into two parts based on a proportion. 2050 | 2051 | Parameters 2052 | ---------- 2053 | left_proportion : float 2054 | Proportion of elements to include in first list (0 < left_proportion < 1) 2055 | 2056 | Returns 2057 | ------- 2058 | Tuple[Slist[A], Slist[A]] 2059 | Tuple of (first part, second part) 2060 | 2061 | Examples 2062 | -------- 2063 | >>> first, second = Slist([1, 2, 3, 4, 5]).split_proportion(0.6) 2064 | >>> first 2065 | Slist([1, 2, 3]) 2066 | >>> second 2067 | Slist([4, 5]) 2068 | """ 2069 | assert 0 < left_proportion < 1, "left_proportion needs to be between 0 and 1" 2070 | left = Slist[A]() 2071 | right = Slist[A]() 2072 | for idx, item in enumerate(self): 2073 | if idx < len(self) * left_proportion: 2074 | left.append(item) 2075 | else: 2076 | right.append(item) 2077 | return left, right 2078 | 2079 | def split_into_n(self, n: int) -> Slist[Slist[A]]: 2080 | """Split list into n roughly equal parts. 2081 | 2082 | Parameters 2083 | ---------- 2084 | n : int 2085 | Number of parts to split into (must be positive) 2086 | 2087 | Returns 2088 | ------- 2089 | Slist[Slist[A]] 2090 | List of n sublists of roughly equal size 2091 | 2092 | Examples 2093 | -------- 2094 | >>> Slist([1, 2, 3, 4, 5]).split_into_n(2) 2095 | Slist([Slist([1, 3, 5]), Slist([2, 4])]) 2096 | """ 2097 | assert n > 0, "n needs to be greater than 0" 2098 | output: Slist[Slist[A]] = Slist() 2099 | for _ in range(n): 2100 | output.append(Slist[A]()) 2101 | for idx, item in enumerate(self): 2102 | output[idx % n].append(item) 2103 | return output 2104 | 2105 | def copy(self) -> Slist[A]: 2106 | """Create a shallow copy of the list. 2107 | 2108 | Returns 2109 | ------- 2110 | Slist[A] 2111 | A new Slist with the same elements 2112 | 2113 | Examples 2114 | -------- 2115 | >>> original = Slist([1, 2, 3]) 2116 | >>> copied = original.copy() 2117 | >>> copied.append(4) 2118 | >>> original # Original is unchanged 2119 | Slist([1, 2, 3]) 2120 | """ 2121 | return Slist(super().copy()) 2122 | 2123 | def repeat_until_size(self, size: int) -> Optional[Slist[A]]: 2124 | """Repeat the list elements until reaching specified size. 2125 | 2126 | Parameters 2127 | ---------- 2128 | size : int 2129 | Target size (must be positive) 2130 | 2131 | Returns 2132 | ------- 2133 | Optional[Slist[A]] 2134 | New list with repeated elements, or None if input is empty 2135 | 2136 | Examples 2137 | -------- 2138 | >>> Slist([1, 2]).repeat_until_size(5) 2139 | Slist([1, 2, 1, 2, 1]) 2140 | >>> Slist([]).repeat_until_size(3) 2141 | None 2142 | """ 2143 | assert size > 0, "size needs to be greater than 0" 2144 | if self.is_empty: 2145 | return None 2146 | else: 2147 | new = Slist[A]() 2148 | while True: 2149 | for item in self: 2150 | if len(new) >= size: 2151 | return new 2152 | else: 2153 | new.append(item) 2154 | 2155 | def repeat_until_size_enumerate(self, size: int) -> Slist[Tuple[int, A]]: 2156 | """Repeat the list elements until reaching specified size, with enumeration. 2157 | 2158 | Parameters 2159 | ---------- 2160 | size : int 2161 | Target size (must be positive) 2162 | 2163 | Returns 2164 | ------- 2165 | Slist[Tuple[int, A]] 2166 | New list with repeated elements and their repetition count 2167 | 2168 | Raises 2169 | ------ 2170 | AssertionError 2171 | If size is not positive 2172 | ValueError 2173 | If input list is empty 2174 | 2175 | Examples 2176 | -------- 2177 | >>> Slist(["a", "b"]).repeat_until_size_enumerate(5) 2178 | Slist([(0, 'a'), (0, 'b'), (1, 'a'), (1, 'b'), (2, 'a')]) 2179 | """ 2180 | assert size > 0, "size needs to be greater than 0" 2181 | if self.is_empty: 2182 | raise ValueError("input needs to be non empty") 2183 | 2184 | new = Slist[Tuple[int, A]]() 2185 | repetition_count = 0 2186 | 2187 | while True: 2188 | for item in self: 2189 | if len(new) >= size: 2190 | return new 2191 | else: 2192 | new.append((repetition_count, item)) 2193 | repetition_count += 1 2194 | 2195 | def repeat_until_size_or_raise(self, size: int) -> Slist[A]: 2196 | """Repeat the list elements until reaching specified size. 2197 | 2198 | Parameters 2199 | ---------- 2200 | size : int 2201 | Target size (must be positive) 2202 | 2203 | Returns 2204 | ------- 2205 | Slist[A] 2206 | New list with repeated elements 2207 | 2208 | Raises 2209 | ------ 2210 | AssertionError 2211 | If size is not positive 2212 | ValueError 2213 | If input list is empty 2214 | 2215 | Examples 2216 | -------- 2217 | >>> Slist([1, 2]).repeat_until_size_or_raise(5) 2218 | Slist([1, 2, 1, 2, 1]) 2219 | """ 2220 | assert size > 0, "size needs to be greater than 0" 2221 | assert not self.is_empty, "input needs to be non empty" 2222 | new = Slist[A]() 2223 | while True: 2224 | for item in self: 2225 | if len(new) >= size: 2226 | return new 2227 | else: 2228 | new.append(item) 2229 | 2230 | @overload 2231 | def zip(self, other: Sequence[B], /) -> Slist[Tuple[A, B]]: ... 2232 | 2233 | @overload 2234 | def zip(self, other1: Sequence[B], other2: Sequence[C], /) -> Slist[Tuple[A, B, C]]: ... 2235 | 2236 | @overload 2237 | def zip(self, other1: Sequence[B], other2: Sequence[C], other3: Sequence[D], /) -> Slist[Tuple[A, B, C, D]]: ... 2238 | 2239 | @overload 2240 | def zip( 2241 | self, other1: Sequence[B], other2: Sequence[C], other3: Sequence[D], other4: Sequence[E], / 2242 | ) -> Slist[Tuple[A, B, C, D, E]]: ... 2243 | 2244 | def zip(self: Sequence[A], *others: Sequence[Any]) -> Slist[Tuple[Any, ...]]: 2245 | """Zip this list with other sequences. 2246 | 2247 | Parameters 2248 | ---------- 2249 | *others : Sequence[B] 2250 | Other sequences to zip with 2251 | 2252 | Returns 2253 | ------- 2254 | Slist[Tuple[A, *Tuple[B, ...]]] 2255 | List of tuples containing elements from all sequences 2256 | 2257 | Raises 2258 | ------ 2259 | TypeError 2260 | If sequences have different lengths 2261 | 2262 | Examples 2263 | -------- 2264 | >>> Slist([1, 2, 3]).zip(Slist(["1", "2", "3"])) 2265 | Slist([(1, "1"), (2, "2"), (3, "3")]) 2266 | >>> Slist([1, 2, 3]).zip(Slist(["1", "2", "3"]), Slist([True, True, True])) 2267 | Slist([(1, "1", True), (2, "2", True), (3, "3", True)]) 2268 | """ 2269 | # Convert to list to check lengths 2270 | if sys.version_info >= (3, 10): 2271 | return Slist(zip(self, *others, strict=True)) 2272 | else: 2273 | return Slist(zip(self, *others)) 2274 | 2275 | @overload 2276 | def zip_cycle(self, other: Sequence[B], /) -> Slist[Tuple[A, B]]: ... 2277 | 2278 | @overload 2279 | def zip_cycle(self, other1: Sequence[B], other2: Sequence[C], /) -> Slist[Tuple[A, B, C]]: ... 2280 | 2281 | @overload 2282 | def zip_cycle( 2283 | self, other1: Sequence[B], other2: Sequence[C], other3: Sequence[D], / 2284 | ) -> Slist[Tuple[A, B, C, D]]: ... 2285 | 2286 | @overload 2287 | def zip_cycle( 2288 | self, other1: Sequence[B], other2: Sequence[C], other3: Sequence[D], other4: Sequence[E], / 2289 | ) -> Slist[Tuple[A, B, C, D, E]]: ... 2290 | 2291 | def zip_cycle(self: Sequence[A], *others: Sequence[Any]) -> Slist[Tuple[Any, ...]]: 2292 | """Zip sequences by cycling shorter ones until all are exhausted. 2293 | 2294 | Unlike regular zip which stops at the shortest sequence, zip_cycle 2295 | repeats shorter sequences cyclically until the longest sequence is exhausted. 2296 | 2297 | Parameters 2298 | ---------- 2299 | *others : Sequence[Any] 2300 | Other sequences to zip with 2301 | 2302 | Returns 2303 | ------- 2304 | Slist[Tuple[Any, ...]] 2305 | List of tuples containing elements from all sequences 2306 | 2307 | Examples 2308 | -------- 2309 | >>> Slist([1, 2, 3]).zip_cycle(['a', 'b']) 2310 | Slist([(1, 'a'), (2, 'b'), (3, 'a')]) 2311 | >>> Slist([1, 2]).zip_cycle(['a', 'b', 'c', 'd']) 2312 | Slist([(1, 'a'), (2, 'b'), (1, 'c'), (2, 'd')]) 2313 | >>> Slist([1, 2, 3]).zip_cycle(['a', 'b'], [10, 20, 30, 40]) 2314 | Slist([(1, 'a', 10), (2, 'b', 20), (3, 'a', 30), (1, 'b', 40)]) 2315 | """ 2316 | all_sequences = [self] + list(others) 2317 | 2318 | # If any sequence is empty, return empty list 2319 | if any(len(seq) == 0 for seq in all_sequences): 2320 | return Slist() 2321 | 2322 | # Find the maximum length 2323 | max_len = max(len(seq) for seq in all_sequences) 2324 | 2325 | # Create cycled iterators for each sequence 2326 | cycled_iterators = [itertools.cycle(seq) for seq in all_sequences] 2327 | 2328 | # Zip them together for max_len items 2329 | result = [] 2330 | for _ in range(max_len): 2331 | result.append(tuple(next(it) for it in cycled_iterators)) 2332 | 2333 | return Slist(result) 2334 | 2335 | def slice_with_bool(self, bools: Sequence[bool]) -> Slist[A]: 2336 | """Slice the list using a sequence of boolean values. 2337 | 2338 | Parameters 2339 | ---------- 2340 | bools : Sequence[bool] 2341 | Boolean sequence indicating which elements to keep 2342 | 2343 | Returns 2344 | ------- 2345 | Slist[A] 2346 | List containing elements where corresponding boolean is True 2347 | 2348 | Examples 2349 | -------- 2350 | >>> Slist([1, 2, 3, 4, 5]).slice_with_bool([True, False, True, False, True]) 2351 | Slist([1, 3, 5]) 2352 | """ 2353 | return Slist(item for item, keep in zip(self, bools) if keep) 2354 | 2355 | def __mul__(self, other: typing.SupportsIndex) -> Slist[A]: 2356 | return Slist(super().__mul__(other)) 2357 | 2358 | @classmethod 2359 | def __get_pydantic_core_schema__(cls, source_type: Any, handler): # type: ignore 2360 | # only called by pydantic v2 2361 | from pydantic_core import core_schema # type: ignore 2362 | 2363 | return core_schema.no_info_after_validator_function(cls, handler(list)) 2364 | 2365 | def find_one_or_raise( 2366 | self, 2367 | predicate: Callable[[A], bool], 2368 | exception: Exception = RuntimeError("Failed to find predicate"), 2369 | ) -> A: 2370 | """Find first element that satisfies a predicate or raise exception. 2371 | 2372 | Parameters 2373 | ---------- 2374 | predicate : Callable[[A], bool] 2375 | Function that returns True for the desired element 2376 | exception : Exception, optional 2377 | Exception to raise if no match found, by default RuntimeError("Failed to find predicate") 2378 | 2379 | Returns 2380 | ------- 2381 | A 2382 | First matching element 2383 | 2384 | Raises 2385 | ------ 2386 | Exception 2387 | If no matching element is found 2388 | 2389 | Examples 2390 | -------- 2391 | >>> Slist([1, 2, 3, 4]).find_one_or_raise(lambda x: x > 3) 2392 | 4 2393 | >>> try: 2394 | ... Slist([1, 2, 3]).find_one_or_raise(lambda x: x > 5) 2395 | ... except RuntimeError as e: 2396 | ... print(str(e)) 2397 | Failed to find predicate 2398 | """ 2399 | result = self.find_one(predicate) 2400 | if result is not None: 2401 | return result 2402 | else: 2403 | raise exception 2404 | 2405 | def permutations_pairs(self) -> Slist[Tuple[A, A]]: 2406 | """Generate all possible pairs of elements, including reversed pairs. 2407 | 2408 | This method uses itertools.permutations with length=2, 2409 | but filters out pairs where both elements are the same. 2410 | 2411 | Returns 2412 | ------- 2413 | Slist[Tuple[A, A]] 2414 | A new Slist containing all pairs of elements 2415 | 2416 | Examples 2417 | -------- 2418 | >>> Slist([1, 2]).permutations_pairs() 2419 | Slist([(1, 2), (2, 1)]) 2420 | >>> Slist([1, 2, 3]).permutations_pairs() 2421 | Slist([(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]) 2422 | >>> Slist([]).permutations_pairs() 2423 | Slist([]) 2424 | >>> Slist([1]).permutations_pairs() 2425 | Slist([]) 2426 | """ 2427 | result = Slist(perm for perm in itertools.permutations(self, 2)) 2428 | return result 2429 | 2430 | def combinations_pairs(self) -> Slist[Tuple[A, A]]: 2431 | """Generate pairs of elements without including reversed pairs. 2432 | 2433 | This method uses itertools.combinations with length=2. 2434 | 2435 | Returns 2436 | ------- 2437 | Slist[Tuple[A, A]] 2438 | A new Slist containing unique pairs of elements 2439 | 2440 | Examples 2441 | -------- 2442 | >>> Slist([1, 2]).combinations_pairs() 2443 | Slist([(1, 2)]) 2444 | >>> Slist([1, 2, 3]).combinations_pairs() 2445 | Slist([(1, 2), (1, 3), (2, 3)]) 2446 | >>> Slist([]).combinations_pairs() 2447 | Slist([]) 2448 | >>> Slist([1]).combinations_pairs() 2449 | Slist([]) 2450 | """ 2451 | result = Slist(itertools.combinations(self, 2)) 2452 | return result 2453 | --------------------------------------------------------------------------------