diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..55170d0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,rst,ini}] +indent_style = space +indent_size = 4 + +[*.{html,json,yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..cb5369e --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,46 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Test the Python package + +on: + workflow_dispatch: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade "pip>=24.2" setuptools + python -m pip install build + python -m pip install twine + python -m pip install --group dev + - name: Run linter + run: ruff check + - name: Check types + run: mypy + - name: Run Tests + run: | + python tests.py + python -m build --sdist + twine check dist/* + sphinx-build -b html docs dist/docs diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..3e91511 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,27 @@ +name: Upload Published Python Package + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index c586728..0c114a8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,26 @@ __pycache__/ .python2/ MANIFEST nameparser.egg-info/ -dummycert.pem build *.egg .coverage dist .idea +Pipfile +Pipfile.lock +.tm_properties + +# virtual environments +.env/ +.venv/ +venv/ +env/ + +# tools +.claude/ +.codex/ +.gemini/ +.pytest_cache/ # docs docs/_* diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3377e1d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + - requirements: docs/requirements.txt diff --git a/.tm_properties b/.tm_properties deleted file mode 100644 index 57e04c4..0000000 --- a/.tm_properties +++ /dev/null @@ -1,2 +0,0 @@ -excludeDirectories = "{$excludeDirectories,dist,*.egg-info,build,docs/_*}" -include = "{$include,.gitignore,.hgignore,.travis.yml}" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dc37c42..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "3.5" - - "3.6" -# command to install dependencies -install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi - - "pip install dill" - - "python setup.py install" -# command to run tests -script: python tests.py -sudo: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..536332e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Install dev dependencies (requires pip >= 24.1) +pip install --group dev + +# Run all tests +python tests.py + +# Run a single test by class/method +python -m unittest tests.HumanNamePythonTests.test_utf8 + +# Debug how a specific name string is parsed (prints HumanName repr) +python tests.py "Dr. Juan Q. Xavier de la Vega III" + +# Build docs +sphinx-build -b html docs dist/docs + +# Build package for release +python setup.py sdist bdist_wheel +twine upload dist/* +``` + +Enable debug logging to see the parser's internal decisions: + +```python +import logging +logging.getLogger('HumanName').setLevel(logging.DEBUG) +``` + +## Architecture + +The library has two layers: `nameparser/config/` (data) and `nameparser/parser.py` (logic). + +### Configuration layer (`nameparser/config/`) + +Each module defines a plain Python set of known name pieces: + +- `titles.py` — `TITLES` (prenominals) and `FIRST_NAME_TITLES` (e.g. "Sir", which treat the following name as first, not last) +- `suffixes.py` — `SUFFIX_ACRONYMS` (with periods, e.g. "M.D.") and `SUFFIX_NOT_ACRONYMS` (e.g. "Jr.") +- `prefixes.py` — `PREFIXES` (lastname particles, e.g. "de", "van") +- `conjunctions.py` — `CONJUNCTIONS` (e.g. "and", "of") used to chain multi-word titles +- `capitalization.py` — `CAPITALIZATION_EXCEPTIONS` mapping (e.g. `{'phd': 'Ph.D.'}`) +- `regexes.py` — compiled regular expressions wrapped in a `TupleManager` + +`config/__init__.py` wraps everything into `SetManager` and `TupleManager` instances inside a `Constants` class. A module-level singleton `CONSTANTS` is shared across all `HumanName` instances by default. + +**Two-tier config pattern**: `CONSTANTS` is global; passing `None` as the second arg to `HumanName` creates a fresh per-instance `Constants()`. After modifying per-instance config you must call `hn.parse_full_name()` again. `SetManager.add()`/`remove()` normalizes inputs to lowercase with no periods, so callers don't need to worry about case. + +### Parser (`nameparser/parser.py`) + +`HumanName` is the single public class. Assigning to `full_name` (or instantiating with a string) triggers `parse_full_name()`. + +Parse flow: +1. `pre_process()` — strips nicknames (parenthesis/quotes) and emoji, fixes "Ph.D." variant spellings +2. Split on commas → 1 part (no comma), 2 parts (suffix-comma or lastname-comma), 3+ parts +3. `parse_pieces()` — splits on spaces, detects dotted abbreviations like "Lt.Gov." and adds them to constants dynamically +4. `join_on_conjunctions()` — merges pieces adjacent to conjunctions into single tokens (e.g. `['Secretary', 'of', 'State']` → `['Secretary of State']`); also joins prefix particles to the following lastname token +5. Iterates pieces, assigning to `title_list`, `first_list`, `middle_list`, `last_list`, `suffix_list` +6. `post_process()` — `handle_firstnames()` swaps first/last when only a title + one name; `handle_capitalization()` applies optional auto-cap + +Each named attribute (`title`, `first`, etc.) is a `@property` that joins its corresponding `_list`. Setters call `_set_list()` which runs the value through `parse_pieces()`, so assigning `hn.last = "de la Vega"` correctly re-parses prefix tokens. + +### Tests (`tests.py`) + +All tests live in a single file. `HumanNameTestBase.m()` is a custom assert helper that prints the original name string on failure. Many test classes group cases by name format type. `TEST_NAMES` is a list of name strings that gets automatically permuted into comma-separated variants as a regression check. When adding a new parsing case, add it to the relevant test class and consider adding the base form to `TEST_NAMES`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e1b8ae..15967e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,35 +4,16 @@ Contributing Development Environment Setup -------------------------------- -There are some exernal dependencies required in order to run the -tests, located in the dev-requirements.txt file. +Install dev dependencies: - pip install -r dev-requirements.txt - -If you are running Python 2.6 you will also need to `pip install unitest2` -in order to run the tests. - -Travis CI ---------- - -[![Build Status](https://travis-ci.org/derek73/python-nameparser.svg?branch=master)](https://travis-ci.org/derek73/python-nameparser) - -The GitHub project is set up with Travis CI. Tests are run -automatically against new code pushes to any branch in the main -repository. Test results may be viewed here: - -https://travis-ci.org/derek73/python-nameparser + uv sync Running Tests --------------- -To run the tests locally, run `python tests.py`. - - python tests.py - -You can also pass a name string to `tests.py` to see how it will be parsed. +You can also pass a name string to `tests.py` to see how it will be parsed: $ python tests.py "Secretary of State Hillary Rodham-Clinton" +CI runs tests against Python 3.10–3.13 via GitHub Actions on every push and pull request. Writing Tests ---------------- -If you make changes, please make sure you include tests with example -names that you want to be parsed correctly. - -It's a good idea to include tests of alternate comma placement formats -of the name to ensure that the 3 code paths for the 3 formats work in -the same way. +If you make changes, please include tests with example names that should parse correctly. -The tests could be MUCH better. If the spirit moves you to design or -implement a much more intelligent test strategy, please know that your -efforts will be welcome and appreciated. - -Unless you add better coverage someplace else, add a few examples of -your names to `TEST_NAMES`. A test attempts to try the 3 different -comma variations of these names automatically and make sure things -don't blow up, so it can be a helpful regression indicator. +It's a good idea to include tests of alternate comma placement formats of the name to ensure that the 3 code paths for the 3 formats work in the same way. +Unless you add better coverage someplace else, add a few examples of your names to `TEST_NAMES`. A test attempts to try the 3 different comma variations of these names automatically and make sure things don't blow up, so it can be a helpful regression indicator. New Releases ------------ -[https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/](Publishing to Pypi Guide) - - $ python setup.py sdist bdist_wheel - $ twine upload dist/* - +Releases are published automatically to PyPI via GitHub Actions. To cut a release, +create and publish a new GitHub Release — the workflow will build and upload the +package using trusted publishing (no API token or twine needed). diff --git a/README.rst b/README.rst index da2265c..15dcac1 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,9 @@ Name Parser =========== -.. image:: https://travis-ci.org/derek73/python-nameparser.svg?branch=master - :target: https://travis-ci.org/derek73/python-nameparser -.. image:: https://badge.fury.io/py/nameparser.svg - :target: http://badge.fury.io/py/nameparser +|Build Status| |PyPI| |PyPI version| |Documentation| -A simple Python (3.2+ & 2.6+) module for parsing human names into their +A simple Python (3.10+) module for parsing human names into their individual components. * hn.title @@ -15,6 +12,8 @@ individual components. * hn.last * hn.suffix * hn.nickname +* hn.surnames *(middle + last)* +* hn.initials *(first initial of each name part)* Supported Name Structures ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -50,11 +49,11 @@ Installation If you want to try out the latest code from GitHub you can install with pip using the command below. -``pip install -e git+git://github.com/derek73/python-nameparser.git#egg=nameparser`` +``pip install -e git+https://github.com/derek73/python-nameparser.git`` -If you're looking for a web service, check out -`eyeseast's nameparse service `_, a -simple Heroku-friendly Flask wrapper for this module. +If you need to handle lists of names, check out +`namesparser `_, a +compliment to this module that handles multiple names in a string. Quick Start Example @@ -135,4 +134,13 @@ https://github.com/derek73/python-nameparser .. _CONTRIBUTING.md: https://github.com/derek73/python-nameparser/tree/master/CONTRIBUTING.md .. _Start a New Issue: https://github.com/derek73/python-nameparser/issues -.. _click here to propose changes to the titles: https://github.com/derek73/python-nameparser/edit/master/nameparser/config/titles.py \ No newline at end of file +.. _click here to propose changes to the titles: https://github.com/derek73/python-nameparser/edit/master/nameparser/config/titles.py + +.. |Build Status| image:: https://github.com/derek73/python-nameparser/actions/workflows/python-package.yml/badge.svg + :target: https://github.com/derek73/python-nameparser/actions/workflows/python-package.yml +.. |PyPI| image:: https://img.shields.io/pypi/v/nameparser.svg + :target: https://pypi.org/project/nameparser/ +.. |Documentation| image:: https://readthedocs.org/projects/nameparser/badge/?version=latest + :target: http://nameparser.readthedocs.io/en/latest/?badge=latest +.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/nameparser.svg + :target: https://pypi.org/project/nameparser/ diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 8aab0b6..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -ipdb -nose>=1.3.7 -coverage>=4.0.3 -dill>=0.2.5 -twine -Sphinx diff --git a/docs/conf.py b/docs/conf.py index 09c29c1..5df5adc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# ruff: noqa: E402 # # Nameparser documentation build configuration file, created by # sphinx-quickstart on Fri May 16 01:29:58 2014. @@ -49,8 +49,8 @@ master_doc = 'index' # General information about the project. -project = u'Nameparser' -copyright = u'{:%Y}, Derek Gulbranson'.format(date.today()) +project = 'Nameparser' +copyright = '{:%Y}, Derek Gulbranson'.format(date.today()) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -223,8 +223,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'Nameparser.tex', u'Nameparser Documentation', - u'Derek Gulbranson', 'manual'), + ('index', 'Nameparser.tex', 'Nameparser Documentation', + 'Derek Gulbranson', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -253,8 +253,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'nameparser', u'Nameparser Documentation', - [u'Derek Gulbranson'], 1) + ('index', 'nameparser', 'Nameparser Documentation', + ['Derek Gulbranson'], 1) ] # If true, show URL addresses after external links. @@ -267,8 +267,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Nameparser', u'Nameparser Documentation', - u'Derek Gulbranson', 'Nameparser', 'A simple python modules for parsing human names into components.', + ('index', 'Nameparser', 'Nameparser Documentation', + 'Derek Gulbranson', 'Nameparser', 'A simple python modules for parsing human names into components.', 'Miscellaneous'), ] diff --git a/docs/customize.rst b/docs/customize.rst index 7442300..1e4f38d 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -39,14 +39,14 @@ instantiate the :py:class:`~nameparser.parser.HumanName` class (see below). Editable attributes of nameparser.config.CONSTANTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* :py:obj:`~nameparser.config.Constants.titles` - Pieces that come before the name. Cannot include things that may be first names -* :py:obj:`~nameparser.config.Constants.first_name_titles` - Titles that, when followed by a single name, that name is a first name, e.g. "King David" -* :py:obj:`~nameparser.config.Constants.suffix_acronyms` - Pieces that come at the end of the name that may or may not have periods separating the letters, e.g. "m.d." -* :py:obj:`~nameparser.config.Constants.suffix_not_acronyms` - Pieces that come at the end of the name that never have periods separating the letters, e.g. "Jr." -* :py:obj:`~nameparser.config.Constants.conjunctions` - Connectors like "and" that join the preceding piece to the following piece. -* :py:obj:`~nameparser.config.Constants.prefixes` - Connectors like "del" and "bin" that join to the following piece but not the preceding -* :py:obj:`~nameparser.config.Constants.capitalization_exceptions` - Dictionary of pieces that do not capitalize the first letter, e.g. "Ph.D" -* :py:obj:`~nameparser.config.Constants.regexes` - Regular expressions used to find words, initials, nicknames, etc. +* :py:data:`~nameparser.config.titles.TITLES` - Pieces that come before the name. Includes all `first_name_titles`. Cannot include things that may be first names. +* :py:data:`~nameparser.config.FIRST_NAME_TITLES` - Titles that, when followed by a single name, that name is a first name, e.g. "King David". +* :py:data:`~nameparser.config.SUFFIX_ACRONYMS` - Pieces that come at the end of the name that may or may not have periods separating the letters, e.g. "m.d.". +* :py:data:`~nameparser.config.SUFFIX_NOT_ACRONYMS` - Pieces that come at the end of the name that never have periods separating the letters, e.g. "Jr.". +* :py:data:`~nameparser.config.conjunctions.CONJUNCTIONS` - Connectors like "and" that join the preceding piece to the following piece. +* :py:data:`~nameparser.config.prefixes.PREFIXES` - Connectors like "del" and "bin" that join to the following piece but not the preceding, similar to titles but can appear anywhere in the name. +* :py:data:`~nameparser.config.CAPITALIZATION_EXCEPTIONS` - Dictionary of pieces that do not capitalize the first letter, e.g. "Ph.D". +* :py:data:`~nameparser.config.regexes.REGEXES` - Regular expressions used to find words, initials, nicknames, etc. Each set of constants comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning the constants for your project. These methods automatically lower case and @@ -57,6 +57,8 @@ Other editable attributes * :py:obj:`~nameparser.config.Constants.string_format` - controls output from `str()` * :py:obj:`~nameparser.config.Constants.empty_attribute_default` - value returned by empty attributes, defaults to empty string +* :py:obj:`~nameparser.config.Constants.capitalize_name` - If set, applies :py:meth:`~nameparser.parser.HumanName.capitalize` to :py:class:`~nameparser.parser.HumanName` instance. +* :py:obj:`~nameparser.config.Constants.force_mixed_case_capitalization` - If set, forces the capitalization of mixed case strings when :py:meth:`~nameparser.parser.HumanName.capitalize` is called. diff --git a/docs/modules.rst b/docs/modules.rst index eaf3240..2056330 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,6 +7,7 @@ HumanName.parser .. py:module:: nameparser.parser .. py:class:: HumanName + :noindex: .. autoclass:: HumanName :members: diff --git a/docs/release_log.rst b/docs/release_log.rst index dbae5db..a0ab7ee 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -1,5 +1,46 @@ Release Log =========== +* 1.1.3 - September 20, 2023 + - Fix case when we have two same prefixes in the name ()#147) +* 1.1.2 - November 13, 2022 + - Add support for attributes in constructor (#140) + - Make HumanName instances hashable (#138) + - Update repr for names with single quotes (#137) +* 1.1.1 - January 28, 2022 + - Fix bug in is_suffix handling of lists (#129) +* 1.1.0 - January 3, 2022 + - Add initials support (#128) + - Add more titles and prefixes (#120, #127, #128, #119) +* 1.0.6 - February 8, 2020 + - Fix Python 3.8 syntax error (#104) +* 1.0.5 - Dec 12, 2019 + - Fix suffix parsing bug in comma parts (#98) + - Fix deprecation warning on Python 3.7 (#94) + - Improved capitalization support of mixed case names (#90) + - Remove "elder" from titles (#96) + - Add post-nominal list from Wikipedia to suffixes (#93) +* 1.0.4 - June 26, 2019 + - Better nickname handling of multiple single quotes (#86) + - full_name attribute now returns formatted string output instead of original string (#87) +* 1.0.3 - April 18, 2019 + - fix sys.stdin usage when stdin doesn't exist (#82) + - support for escaping log entry arguments (#84) +* 1.0.2 - Oct 26, 2018 + - Fix handling of only nickname and last name (#78) +* 1.0.1 - August 30, 2018 + - Fix overzealous regex for "Ph. D." (#43) + - Add `surnames` attribute as aggregate of middle and last names +* 1.0.0 - August 30, 2018 + - Fix support for nicknames in single quotes (#74) + - Change prefix handling to support prefixes on first names (#60) + - Fix prefix capitalization when not part of lastname (#70) + - Handle erroneous space in "Ph. D." (#43) +* 0.5.8 - August 19, 2018 + - Add "Junior" to suffixes (#76) + - Add "dra" and "srta" to titles (#77) +* 0.5.7 - June 16, 2018 + - Fix doc link (#73) + - Fix handling of "do" and "dos" Portuguese prefixes (#71, #72) * 0.5.6 - January 15, 2018 - Fix python version check (#64) * 0.5.5 - January 10, 2018 @@ -82,7 +123,7 @@ Release Log - Generate documentation using sphinx and host on readthedocs. * 0.2.10 - May 6, 2014 - If name is only a title and one part, assume it's a last name instead of a first name, with exceptions for some titles like 'Sir'. (`#7 `_). - - Add some judicial and other common titles. (#9) + - Add some judicial and other common titles. (#9) * 0.2.9 - Apr 1, 2014 - Add a new nickname attribute containing anything in parenthesis or double quotes (`Issue 33 `_). * 0.2.8 - Oct 25, 2013 @@ -95,7 +136,7 @@ Release Log * 0.2.5 - Feb 11, 2013 - Set logging handler to NullHandler - Remove 'ben' from PREFIXES because it's more common as a name than a prefix. - - Deprecate BlankHumanNameError. Do not raise exceptions if full_name is empty string. + - Deprecate BlankHumanNameError. Do not raise exceptions if full_name is empty string. * 0.2.4 - Feb 10, 2013 - Adjust logging, don't set basicConfig. Fix `Issue 10 `_ and `Issue 26 `_. - Fix handling of single lower case initials that are also conjunctions, e.g. "john e smith". Re `Issue 11 `_. @@ -106,12 +147,12 @@ Release Log - tests/test.py can now take an optional name argument that will return repr() for that name. * 0.2.3 - Fix overzealous "Mac" regex * 0.2.2 - Fix parsing error -* 0.2.0 +* 0.2.0 - Significant refactor of parsing logic. Handle conjunctions and prefixes before parsing into attribute buckets. - Support attribute overriding by assignment. - - Support multiple titles. - - Lowercase titles constants to fix bug with comparison. + - Support multiple titles. + - Lowercase titles constants to fix bug with comparison. - Move documentation to README.rst, add release log. * 0.1.4 - Use set() in constants for improved speed. setuptools compatibility - sketerpot * 0.1.3 - Add capitalization feature - twotwo diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..c8ee782 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +Sphinx +alabaster diff --git a/docs/resources.rst b/docs/resources.rst index 0c70695..8934aae 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -2,13 +2,19 @@ Naming Practices and Resources ============================== * US_Census_Surname_Data_2000_ + * US_Social_Security_Administration_Baby_Names_Index_ * Naming_practice_guide_UK_2006_ * Wikipedia_Anthroponymy_ * Wikipedia_Naming_conventions_ * Wikipedia_List_Of_Titles_ + * Tussenvoegsel_ + * Family_Name_Affixes_ -.. _US_Census_Surname_Data_2000: http://www.census.gov/genealogy/www/data/2000surnames/index.html +.. _US_Census_Surname_Data_2000: https://www.census.gov/data/developers/data-sets/surnames/2000.html +.. _US_Social_Security_Administration_Baby_Names_Index: https://www.ssa.gov/oact/babynames/limits.html .. _Naming_practice_guide_UK_2006: https://www.fbiic.gov/public/2008/nov/Naming_practice_guide_UK_2006.pdf .. _Wikipedia_Anthroponymy: https://en.wikipedia.org/wiki/Anthroponymy .. _Wikipedia_Naming_conventions: http://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(people) .. _Wikipedia_List_Of_Titles: https://en.wikipedia.org/wiki/Title +.. _Tussenvoegsel: https://en.wikipedia.org/wiki/Tussenvoegsel +.. _Family_Name_Affixes : https://en.wikipedia.org/wiki/List_of_family_name_affixes diff --git a/docs/usage.rst b/docs/usage.rst index f3ab41b..0a89098 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -4,7 +4,7 @@ Using the HumanName Parser Example Usage ------------- -The examples use Python 3, but Python 2.6+ is supported. +Requires Python 3.10+. .. doctest:: :options: +NORMALIZE_WHITESPACE @@ -23,6 +23,8 @@ The examples use Python 3, but Python 2.6+ is supported. 'de la Vega' >>> name.suffix 'III' + >>> name.surnames + 'Q. Xavier de la Vega' >>> name.full_name = "Juan Q. Xavier Velasquez y Garcia, Jr." >>> name >> str(name) 'Shirley MacLaine' +To apply capitalization to all `HumanName` instances, set +:py:attr:`~nameparser.config.Constants.capitalize_name` to `True`. + +.. doctest:: capitalize_name + :options: +NORMALIZE_WHITESPACE + + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.capitalize_name = True + >>> name = HumanName("bob v. de la macdole-eisenhower phd") + >>> str(name) + 'Bob V. de la MacDole-Eisenhower Ph.D.' + +To force the capitalization of mixed case strings on all `HumanName` instances, +set :py:attr:`~nameparser.config.Constants.force_mixed_case_capitalization` to `True`. + +.. doctest:: force_mixed_case_capitalization + :options: +NORMALIZE_WHITESPACE + + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.force_mixed_case_capitalization = True + >>> name = HumanName('Shirley Maclaine') + >>> name.capitalize() + >>> str(name) + 'Shirley MacLaine' + Nickname Handling ------------------ -The content of parenthesis or double quotes in the name will be +The content of parenthesis or quotes in the name will be available from the nickname attribute. .. doctest:: nicknames @@ -150,3 +176,41 @@ Don't want to include nicknames in your output? No problem. Just omit that keywo 'Dr. Juan de la Vega' +Initials Support +---------------- + +The HumanName class can try to get the correct representation of initials. +Initials can be tricky as different format usages exist. +To exclude any of the name parts from the initials, change the initials format string: +:py:attr:`~nameparser.config.Constants.initials_format` +Three attributes exist for the format, `first`, `middle` and `last`. + +.. doctest:: initials format + + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.initials_format = "{first} {middle}" + >>> HumanName("Doe, John A. Kenneth, Jr.").initials() + 'J. A. K.' + >>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{last}, {first}).initials() + 'D., J.' + + +Furthermore, the delimiter for the string output can be set through: +:py:attr:`~nameparser.config.Constants.initials_delimiter` + +.. doctest:: initials delimiter + + >>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";").initials() + "J; A; K;" + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.initials_delimiter = "." + >>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first}{middle}{last}).initials() + "J.A.K.D." + +To get a list representation of the initials, use :py:meth:`~nameparser.HumanName.initials_list`. +This function is unaffected by :py:attr:`~nameparser.config.Constants.initials_format` + +.. doctest:: list format + >>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";").initials_list() + ["J", "A", "K", "D"] + diff --git a/nameparser/__init__.py b/nameparser/__init__.py index d3efaaf..c82d800 100644 --- a/nameparser/__init__.py +++ b/nameparser/__init__.py @@ -1,9 +1,7 @@ -VERSION = (0, 5, 6) -__version__ = '.'.join(map(str, VERSION)) +from nameparser._version import VERSION as VERSION +from nameparser._version import __version__ as __version__ +from nameparser.parser import HumanName as HumanName __author__ = "Derek Gulbranson" __author_email__ = 'derek73@gmail.com' __license__ = "LGPL" __url__ = "https://github.com/derek73/python-nameparser" - - -from nameparser.parser import HumanName diff --git a/nameparser/_version.py b/nameparser/_version.py new file mode 100644 index 0000000..b04f37e --- /dev/null +++ b/nameparser/_version.py @@ -0,0 +1,2 @@ +VERSION = (1, 1, 3) +__version__ = '.'.join(map(str, VERSION)) diff --git a/nameparser/config/__init__.py b/nameparser/config/__init__.py index 3b11e88..ce9da44 100644 --- a/nameparser/config/__init__.py +++ b/nameparser/config/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The :py:mod:`nameparser.config` module manages the configuration of the nameparser. @@ -11,7 +10,7 @@ >>> from nameparser.config import CONSTANTS >>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +ELLIPSIS - SetManager(set([u'msgt', ..., u'adjutant'])) + SetManager({'msgt', ..., 'adjutant'}) You can also adjust the configuration of individual instances by passing ``None`` as the second argument upon instantiation. @@ -21,18 +20,23 @@ >>> from nameparser import HumanName >>> hn = HumanName("Dean Robert Johns", None) >>> hn.C.titles.add('dean') # doctest: +ELLIPSIS - SetManager(set([u'msgt', ..., u'adjutant'])) + SetManager({'msgt', ..., 'adjutant'}) >>> hn.parse_full_name() # need to run this again after config changes **Potential Gotcha**: If you do not pass ``None`` as the second argument, ``hn.C`` will be a reference to the module config, possibly yielding unexpected results. See `Customizing the Parser `_. """ -from __future__ import unicode_literals -import collections +import re import sys +from collections.abc import Iterable, Iterator, Mapping, Set +from typing import Any, TypeVar + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self -from nameparser.util import binary_type from nameparser.util import lc from nameparser.config.prefixes import PREFIXES from nameparser.config.capitalization import CAPITALIZATION_EXCEPTIONS @@ -41,99 +45,108 @@ from nameparser.config.suffixes import SUFFIX_NOT_ACRONYMS from nameparser.config.titles import TITLES from nameparser.config.titles import FIRST_NAME_TITLES -from nameparser.config.regexes import REGEXES +from nameparser.config.regexes import EMPTY_REGEX, REGEXES DEFAULT_ENCODING = 'UTF-8' -class SetManager(collections.Set): + +class SetManager(Set): ''' Easily add and remove config variables per module or instance. Subclass of - ``collections.Set``. - + ``collections.abc.Set``. + Only special functionality beyond that provided by set() is to normalize constants for comparison (lower case, no periods) when they are add()ed and remove()d and allow passing multiple string arguments to the :py:func:`add()` and :py:func:`remove()` methods. - + ''' - def __init__(self, elements): + + def __init__(self, elements: Iterable[str]) -> None: self.elements = set(elements) - - def __call__(self): + + def __call__(self) -> Set[str]: return self.elements - - def __repr__(self): - return "SetManager({})".format(self.elements) # used for docs - - def __iter__(self): + + def __repr__(self) -> str: + return "SetManager({})".format(self.elements) # used for docs + + def __iter__(self) -> Iterator[str]: return iter(self.elements) - - def __contains__(self, value): + + def __contains__(self, value: object) -> bool: return value in self.elements - - def __len__(self): + + def __len__(self) -> int: return len(self.elements) - - def next(self): - return self.__next__() - - def __next__(self): - if self.count >= len(self.elements): - self.count = 0 - raise StopIteration - else: - c = self.count - self.count = c + 1 - return getattr(self, self.elements[c]) or next(self) - - def add_with_encoding(self, s, encoding=None): + + def add_with_encoding(self, s: str, encoding: str | None = None) -> None: """ Add the lower case and no-period version of the string to the set. Pass an explicit `encoding` parameter to specify the encoding of binary strings that are not DEFAULT_ENCODING (UTF-8). """ - encoding = encoding or sys.stdin.encoding or DEFAULT_ENCODING - if type(s) == binary_type: + stdin_encoding = None + if sys.stdin: + stdin_encoding = sys.stdin.encoding + encoding = encoding or stdin_encoding or DEFAULT_ENCODING + if isinstance(s, bytes): s = s.decode(encoding) self.elements.add(lc(s)) - def add(self, *strings): + def add(self, *strings: str) -> Self: """ Add the lower case and no-period version of the string arguments to the set. Can pass a list of strings. Returns ``self`` for chaining. """ - [self.add_with_encoding(s) for s in strings] + for s in strings: + self.add_with_encoding(s) + return self - - def remove(self, *strings): + + def remove(self, *strings: str) -> Self: """ Remove the lower case and no-period version of the string arguments from the set. Returns ``self`` for chaining. """ - [self.elements.remove(lc(s)) for s in strings if lc(s) in self.elements] + for s in strings: + if (lower := lc(s)) in self.elements: + self.elements.remove(lower) + return self -class TupleManager(dict): +T = TypeVar('T') + + +class TupleManager(dict[str, T]): ''' A dictionary with dot.notation access. Subclass of ``dict``. Makes the tuple constants more friendly. ''' - def __getattr__(self, attr): + + def __getattr__(self, attr: str) -> T | None: return self.get(attr) - __setattr__= dict.__setitem__ - __delattr__= dict.__delitem__ - def __getstate__(self): + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + def __getstate__(self) -> Mapping[str, T]: return dict(self) - def __setstate__(self, state): - self.__init__(state) + def __setstate__(self, state: Mapping[str, T]) -> None: + self.update(state) - def __reduce__(self): + def __reduce__(self) -> tuple[type, tuple[()], Mapping[str, T]]: return (TupleManager, (), self.__getstate__()) -class Constants(object): + +class RegexTupleManager(TupleManager[re.Pattern[str]]): + def __getattr__(self, attr: str) -> re.Pattern[str]: + return self.get(attr, EMPTY_REGEX) + + +class Constants: """ An instance of this class hold all of the configuration constants for the parser. @@ -156,17 +169,40 @@ class Constants(object): :param regexes: :py:attr:`regexes` wrapped with :py:class:`TupleManager`. """ - + + prefixes: SetManager + suffix_acronyms: SetManager + suffix_not_acronyms: SetManager + titles: SetManager + first_name_titles: SetManager + conjunctions: SetManager + capitalization_exceptions: TupleManager[str] + regexes: RegexTupleManager + + _pst: Set[str] | None + string_format = "{title} {first} {middle} {last} {suffix} ({nickname})" """ The default string format use for all new `HumanName` instances. """ + + initials_format = "{first} {middle} {last}" + """ + The default initials format used for all new `HumanName` instances. + """ + + initials_delimiter = "." + """ + The default initials delimiter used for all new `HumanName` instances. + Will be used to add a delimiter between each initial. + """ + empty_attribute_default = '' """ Default return value for empty attributes. - + .. doctest:: - + >>> from nameparser.config import CONSTANTS >>> CONSTANTS.empty_attribute_default = None >>> name = HumanName("John Doe") @@ -174,47 +210,78 @@ class Constants(object): None >>>name.first 'John' - + + """ + + capitalize_name = False + """ + If set, applies :py:meth:`~nameparser.parser.HumanName.capitalize` to + :py:class:`~nameparser.parser.HumanName` instance. + + .. doctest:: + + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.capitalize_name = True + >>> name = HumanName("bob v. de la macdole-eisenhower phd") + >>> str(name) + 'Bob V. de la MacDole-Eisenhower Ph.D.' + """ - - - def __init__(self, - prefixes=PREFIXES, - suffix_acronyms=SUFFIX_ACRONYMS, - suffix_not_acronyms=SUFFIX_NOT_ACRONYMS, - titles=TITLES, - first_name_titles=FIRST_NAME_TITLES, - conjunctions=CONJUNCTIONS, - capitalization_exceptions=CAPITALIZATION_EXCEPTIONS, - regexes=REGEXES - ): - self.prefixes = SetManager(prefixes) - self.suffix_acronyms = SetManager(suffix_acronyms) + + force_mixed_case_capitalization = False + """ + If set, forces the capitalization of mixed case strings when + :py:meth:`~nameparser.parser.HumanName.capitalize` is called. + + .. doctest:: + + >>> from nameparser.config import CONSTANTS + >>> CONSTANTS.force_mixed_case_capitalization = True + >>> name = HumanName('Shirley Maclaine') + >>> name.capitalize() + >>> str(name) + 'Shirley MacLaine' + + """ + + def __init__(self, + prefixes: Iterable[str] = PREFIXES, + suffix_acronyms: Iterable[str] = SUFFIX_ACRONYMS, + suffix_not_acronyms: Iterable[str] = SUFFIX_NOT_ACRONYMS, + titles: Iterable[str] = TITLES, + first_name_titles: Iterable[str] = FIRST_NAME_TITLES, + conjunctions: Iterable[str] = CONJUNCTIONS, + capitalization_exceptions: TupleManager[str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS, + regexes: RegexTupleManager | TupleManager[re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES + ) -> None: + self.prefixes = SetManager(prefixes) + self.suffix_acronyms = SetManager(suffix_acronyms) self.suffix_not_acronyms = SetManager(suffix_not_acronyms) - self.titles = SetManager(titles) - self.first_name_titles = SetManager(first_name_titles) - self.conjunctions = SetManager(conjunctions) + self.titles = SetManager(titles) + self.first_name_titles = SetManager(first_name_titles) + self.conjunctions = SetManager(conjunctions) self.capitalization_exceptions = TupleManager(capitalization_exceptions) - self.regexes = TupleManager(regexes) + self.regexes = RegexTupleManager(regexes) self._pst = None - + @property - def suffixes_prefixes_titles(self): + def suffixes_prefixes_titles(self) -> Set[str]: if not self._pst: self._pst = self.prefixes | self.suffix_acronyms | self.suffix_not_acronyms | self.titles return self._pst - def __repr__(self): + def __repr__(self) -> str: return "" - - def __setstate__(self, state): - self.__init__(state) - - def __getstate__(self): + + def __setstate__(self, state: Mapping[str, Any]) -> None: + Constants.__init__(self, state) + + def __getstate__(self) -> Mapping[str, Any]: attrs = [x for x in dir(self) if not x.startswith('_')] - return dict([(a,getattr(self, a)) for a in attrs]) + return dict([(a, getattr(self, a)) for a in attrs]) + -#: A module-level instance of the :py:class:`Constants()` class. +#: A module-level instance of the :py:class:`Constants()` class. #: Provides a common instance for the module to share #: to easily adjust configuration for the entire module. #: See `Customizing the Parser with Your Own Configuration `_. diff --git a/nameparser/config/capitalization.py b/nameparser/config/capitalization.py index 4aa3214..0172691 100644 --- a/nameparser/config/capitalization.py +++ b/nameparser/config/capitalization.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - CAPITALIZATION_EXCEPTIONS = ( - ('ii' ,'II'), - ('iii','III'), - ('iv' ,'IV'), - ('md' ,'M.D.'), - ('phd','Ph.D.'), + ('ii', 'II'), + ('iii', 'III'), + ('iv', 'IV'), + ('md', 'M.D.'), + ('phd', 'Ph.D.'), ) """ Any pieces that are not capitalized by capitalizing the first letter. -""" \ No newline at end of file +""" diff --git a/nameparser/config/conjunctions.py b/nameparser/config/conjunctions.py index fb89201..695c9ee 100644 --- a/nameparser/config/conjunctions.py +++ b/nameparser/config/conjunctions.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - CONJUNCTIONS = set([ '&', 'and', @@ -15,4 +12,4 @@ Pieces that should join to their neighboring pieces, e.g. "and", "y" and "&". "of" and "the" are also include to facilitate joining multiple titles, e.g. "President of the United States". -""" \ No newline at end of file +""" diff --git a/nameparser/config/prefixes.py b/nameparser/config/prefixes.py index 21c82fa..9e0e772 100644 --- a/nameparser/config/prefixes.py +++ b/nameparser/config/prefixes.py @@ -1,14 +1,21 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -#: Name pieces that appear before a last name. They join to the piece that follows them to make one new piece. +#: Name pieces that appear before a last name. Prefixes join to the piece +#: that follows them to make one new piece. They can be chained together, e.g +#: "von der" and "de la". Because they only appear in middle or last names, +#: they also signify that all following name pieces should be in the same name +#: part, for example, "von" will be joined to all following pieces that are not +#: prefixes or suffixes, allowing recognition of double last names when they +#: appear after a prefixes. So in "pennie von bergen wessels MD", "von" will +#: join with all following name pieces until the suffix "MD", resulting in the +#: correct parsing of the last name "von bergen wessels". PREFIXES = set([ 'abu', + 'al', 'bin', 'bon', 'da', 'dal', 'de', + 'de\'', 'degli', 'dei', 'del', @@ -19,16 +26,22 @@ 'dello', 'der', 'di', - 'du', 'dí', + 'do', + 'dos', + 'du', 'ibn', 'la', 'le', + 'mac', + 'mc', 'san', 'santa', 'st', 'ste', 'van', + 'vander', 'vel', 'von', + 'vom', ]) diff --git a/nameparser/config/regexes.py b/nameparser/config/regexes.py index 42da85d..e972f9e 100644 --- a/nameparser/config/regexes.py +++ b/nameparser/config/regexes.py @@ -1,33 +1,27 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import re # emoji regex from https://stackoverflow.com/questions/26568722/remove-unicode-emoji-using-re-in-python -try: - # Wide UCS-4 build - re_emoji = re.compile('[' - '\U0001F300-\U0001F64F' - '\U0001F680-\U0001F6FF' - '\u2600-\u26FF\u2700-\u27BF]+', - re.UNICODE) -except re.error: - # Narrow UCS-2 build - re_emoji = re.compile('(' - '\ud83c[\udf00-\udfff]|' - '\ud83d[\udc00-\ude4f\ude80-\udeff]|' - '[\u2600-\u26FF\u2700-\u27BF])+', - re.UNICODE) +re_emoji = re.compile('[' # lgtm[py/overly-large-range] + '\U0001F300-\U0001F64F' + '\U0001F680-\U0001F6FF' + '\u2600-\u26FF\u2700-\u27BF]+', + re.UNICODE) + +EMPTY_REGEX = re.compile('') REGEXES = set([ ("spaces", re.compile(r"\s+", re.U)), ("word", re.compile(r"(\w|\.)+", re.U)), ("mac", re.compile(r'^(ma?c)(\w{2,})', re.I | re.U)), ("initial", re.compile(r'^(\w\.|[A-Z])?$', re.U)), - ("nickname", re.compile(r'\s*?[\("](.+?)[\)"]', re.U)), + ("quoted_word", re.compile(r'(? list[tuple[int, int]]: """ - return list of tuples containing first and last index + return list of tuples containing first and last index position of contiguous numbers in a series """ - ranges = [] - for key, group in groupby(enumerate(data), lambda i: i[0] - i[1]): - group = list(map(itemgetter(1), group)) + ranges: list[tuple[int, int]] = [] + for key, group_with_indices in groupby(enumerate(data), lambda i: i[0] - i[1]): + group = list(map(itemgetter(1), group_with_indices)) if len(group) > 1: ranges.append((group[0], group[-1])) return ranges -class HumanName(object): + +class HumanName: """ Parse a person's name into individual components. - + Instantiation assigns to ``full_name``, and assignment to :py:attr:`full_name` triggers :py:func:`parse_full_name`. After parsing the - name, these instance attributes are available. - + name, these instance attributes are available. Alternatively, you can pass + any of the instance attributes to the constructor method and skip the parsing + process. If any of the the instance attributes are passed to the constructor + as keywords, :py:func:`parse_full_name` will not be performed. + **HumanName Instance Attributes** - + * :py:attr:`title` * :py:attr:`first` * :py:attr:`middle` * :py:attr:`last` * :py:attr:`suffix` * :py:attr:`nickname` - + * :py:attr:`surnames` + :param str full_name: The name string to be parsed. - :param constants constants: - a :py:class:`~nameparser.config.Constants` instance. Pass ``None`` for - `per-instance config `_. + :param constants constants: + a :py:class:`~nameparser.config.Constants` instance. Pass ``None`` for + `per-instance config `_. :param str encoding: string representing the encoding of your input - :param str string_format: python string formatting + :param str string_format: python string formatting + :param str initials_format: python initials string formatting + :param str initials_delimter: string delimiter for initials + :param str first: first name + :param str middle: middle name + :param str last: last name + :param str title: The title or prenominal + :param str suffix: The suffix or postnominal + :param str nickname: Nicknames """ - + C = CONSTANTS """ A reference to the configuration for this instance, which may or may not be - a reference to the shared, module-wide instance at - :py:mod:`~nameparser.config.CONSTANTS`. See `Customizing the Parser + a reference to the shared, module-wide instance at + :py:mod:`~nameparser.config.CONSTANTS`. See `Customizing the Parser `_. """ - - original = '' + + original: str | bytes = '' """ The original string, untouched by the parser. """ - + _count = 0 - _members = ['title','first','middle','last','suffix','nickname'] + _members = ['title', 'first', 'middle', 'last', 'suffix', 'nickname'] unparsable = True _full_name = '' - - def __init__(self, full_name="", constants=CONSTANTS, encoding=DEFAULT_ENCODING, - string_format=None): + + title_list: list[str] + first_list: list[str] + middle_list: list[str] + last_list: list[str] + suffix_list: list[str] + nickname_list: list[str] + + def __init__( + self, + full_name: str | bytes = "", + constants: Constants = CONSTANTS, + encoding: str = DEFAULT_ENCODING, + string_format: str | None = None, + initials_format: str | None = None, + initials_delimiter: str | None = None, + first: str | list[str] | None = None, + middle: str | list[str] | None = None, + last: str | list[str] | None = None, + title: str | list[str] | None = None, + suffix: str | list[str] | None = None, + nickname: str | list[str] | None = None, + ) -> None: self.C = constants if type(self.C) is not type(CONSTANTS): self.C = Constants() - + self.encoding = encoding self.string_format = string_format or self.C.string_format - # full_name setter triggers the parse - self.full_name = full_name - - def __iter__(self): + self.initials_format = initials_format or self.C.initials_format + self.initials_delimiter = initials_delimiter or self.C.initials_delimiter + if (first or middle or last or title or suffix or nickname): + self.first = first + self.middle = middle + self.last = last + self.title = title + self.suffix = suffix + self.nickname = nickname + self.unparsable = False + else: + # full_name setter triggers the parse + self.full_name = full_name + + def __iter__(self) -> Iterator[str]: return self - - def __len__(self): + + def __len__(self) -> int: l = 0 for x in self: l += 1 return l - - def __eq__(self, other): + + def __eq__(self, other: object) -> bool: """ - HumanName instances are equal to other objects whose + HumanName instances are equal to other objects whose lower case unicode representation is the same. """ - return (u(self)).lower() == (u(other)).lower() - - def __ne__(self, other): - return not (u(self)).lower() == (u(other)).lower() - - def __getitem__(self, key): + return str(self).lower() == str(other).lower() + + def __ne__(self, other: object) -> bool: + return not str(self).lower() == str(other).lower() + + @overload + def __getitem__(self, key: slice) -> list[str]: ... + @overload + def __getitem__(self, key: str) -> str: ... + def __getitem__(self, key: slice | str) -> str | list[str]: if isinstance(key, slice): return [getattr(self, x) for x in self._members[key]] else: return getattr(self, key) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: str) -> None: if key in self._members: self._set_list(key, value) else: raise KeyError("Not a valid HumanName attribute", key) - def next(self): - return self.__next__() - - def __next__(self): + def __next__(self) -> str: if self._count >= len(self._members): self._count = 0 raise StopIteration @@ -124,25 +165,23 @@ def __next__(self): self._count = c + 1 return getattr(self, self._members[c]) or next(self) - def __unicode__(self): + def __str__(self) -> str: if self.string_format: # string_format = "{title} {first} {middle} {last} {suffix} ({nickname})" _s = self.string_format.format(**self.as_dict()) # remove trailing punctuation from missing nicknames - _s = _s.replace(str(self.C.empty_attribute_default),'').replace(" ()","").replace(" ''","").replace(' ""',"") + _s = _s.replace(str(self.C.empty_attribute_default), '').replace(" ()", "").replace(" ''", "").replace(' ""', "") return self.collapse_whitespace(_s).strip(', ') return " ".join(self) - - def __str__(self): - if sys.version_info[0] >= 3: - return self.__unicode__() - return self.__unicode__().encode(self.encoding) - - def __repr__(self): + + def __hash__(self) -> int: + return hash(str(self)) + + def __repr__(self) -> str: if self.unparsable: - _string = "<%(class)s : [ Unparsable ] >" % {'class': self.__class__.__name__,} + _string = "<%(class)s : [ Unparsable ] >" % {'class': self.__class__.__name__, } else: - _string = "<%(class)s : [\n\ttitle: '%(title)s' \n\tfirst: '%(first)s' \n\tmiddle: '%(middle)s' \n\tlast: '%(last)s' \n\tsuffix: '%(suffix)s'\n\tnickname: '%(nickname)s'\n]>" % { + _string = "<%(class)s : [\n\ttitle: %(title)r \n\tfirst: %(first)r \n\tmiddle: %(middle)r \n\tlast: %(last)r \n\tsuffix: %(suffix)r\n\tnickname: %(nickname)r\n]>" % { 'class': self.__class__.__name__, 'title': self.title or '', 'first': self.first or '', @@ -151,25 +190,23 @@ def __repr__(self): 'suffix': self.suffix or '', 'nickname': self.nickname or '', } - if sys.version_info[0] >= 3: - return _string - return _string.encode(self.encoding) - - def as_dict(self, include_empty=True): + return _string + + def as_dict(self, include_empty: bool = True) -> dict[str, str]: """ Return the parsed name as a dictionary of its attributes. - + :param bool include_empty: Include keys in the dictionary for empty name attributes. :rtype: dict - + .. doctest:: - + >>> name = HumanName("Bob Dole") >>> name.as_dict() {'last': 'Dole', 'suffix': '', 'title': '', 'middle': '', 'nickname': '', 'first': 'Bob'} >>> name.as_dict(False) {'last': 'Dole', 'first': 'Bob'} - + """ d = {} for m in self._members: @@ -180,221 +217,341 @@ def as_dict(self, include_empty=True): if val: d[m] = val return d - + + def __process_initial__(self, name_part: str, firstname: bool = False) -> str: + """ + Name parts may include prefixes or conjunctions. This function filters these from the name unless it is + a first name, since first names cannot be conjunctions or prefixes. + """ + parts = name_part.split(" ") + initials = [] + if len(parts) and isinstance(parts, list): + for part in parts: + if not (self.is_prefix(part) or self.is_conjunction(part)) or firstname: + initials.append(part[0]) + if len(initials) > 0: + return " ".join(initials) + else: + return self.C.empty_attribute_default + + def initials_list(self) -> list[str]: + """ + Returns the initials as a list + + .. doctest:: + + >>> name = HumanName("Sir Bob Andrew Dole") + >>> name.initials_list() + ["B", "A", "D"] + >>> name = HumanName("J. Doe") + >>> name.initials_list() + ["J", "D"] + """ + first_initials_list = [self.__process_initial__(name, True) for name in self.first_list if name] + middle_initials_list = [self.__process_initial__(name) for name in self.middle_list if name] + last_initials_list = [self.__process_initial__(name) for name in self.last_list if name] + return first_initials_list + middle_initials_list + last_initials_list + + def initials(self) -> str: + """ + Return period-delimited initials of the first, middle and optionally last name. + + :param bool include_last_name: Include the last name as part of the initials + :rtype: str + + .. doctest:: + + >>> name = HumanName("Sir Bob Andrew Dole") + >>> name.initials() + "B. A. D." + >>> name = HumanName("Sir Bob Andrew Dole", initials_format="{first} {middle}") + >>> name.initials() + "B. A." + """ + + first_initials_list = [self.__process_initial__(name, True) for name in self.first_list if name] + middle_initials_list = [self.__process_initial__(name) for name in self.middle_list if name] + last_initials_list = [self.__process_initial__(name) for name in self.last_list if name] + + initials_dict = { + "first": (self.initials_delimiter + " ").join(first_initials_list) + self.initials_delimiter + if len(first_initials_list) else self.C.empty_attribute_default, + "middle": (self.initials_delimiter + " ").join(middle_initials_list) + self.initials_delimiter + if len(middle_initials_list) else self.C.empty_attribute_default, + "last": (self.initials_delimiter + " ").join(last_initials_list) + self.initials_delimiter + if len(last_initials_list) else self.C.empty_attribute_default + } + + _s = self.initials_format.format(**initials_dict) + return self.collapse_whitespace(_s) + @property - def has_own_config(self): + def has_own_config(self) -> bool: """ - True if this instance is not using the shared module-level + True if this instance is not using the shared module-level configuration. """ return self.C is not CONSTANTS - - ### attributes - + + # attributes + @property - def title(self): + def title(self) -> str: """ - The person's titles. Any string of consecutive pieces in - :py:mod:`~nameparser.config.titles` or + The person's titles. Any string of consecutive pieces in + :py:mod:`~nameparser.config.titles` or :py:mod:`~nameparser.config.conjunctions` at the beginning of :py:attr:`full_name`. """ return " ".join(self.title_list) or self.C.empty_attribute_default - + + @title.setter + def title(self, value: str | list[str] | None) -> None: + self._set_list('title', value) + @property - def first(self): + def first(self) -> str: """ - The person's first name. The first name piece after any known + The person's first name. The first name piece after any known :py:attr:`title` pieces parsed from :py:attr:`full_name`. """ return " ".join(self.first_list) or self.C.empty_attribute_default - + + @first.setter + def first(self, value: str | list[str] | None) -> None: + self._set_list('first', value) + @property - def middle(self): + def middle(self) -> str: """ - The person's middle names. All name pieces after the first name and + The person's middle names. All name pieces after the first name and before the last name parsed from :py:attr:`full_name`. """ return " ".join(self.middle_list) or self.C.empty_attribute_default - + + @middle.setter + def middle(self, value: str | list[str] | None) -> None: + self._set_list('middle', value) + @property - def last(self): + def last(self) -> str: """ - The person's last name. The last name piece parsed from + The person's last name. The last name piece parsed from :py:attr:`full_name`. """ return " ".join(self.last_list) or self.C.empty_attribute_default - + + @last.setter + def last(self, value: str | list[str] | None) -> None: + self._set_list('last', value) + @property - def suffix(self): + def suffix(self) -> str: """ The persons's suffixes. Pieces at the end of the name that are found in :py:mod:`~nameparser.config.suffixes`, or pieces that are at the end - of comma separated formats, e.g. - "Lastname, Title Firstname Middle[,] Suffix [, Suffix]" parsed + of comma separated formats, e.g. + "Lastname, Title Firstname Middle[,] Suffix [, Suffix]" parsed from :py:attr:`full_name`. """ return ", ".join(self.suffix_list) or self.C.empty_attribute_default - + + @suffix.setter + def suffix(self, value: str | list[str] | None) -> None: + self._set_list('suffix', value) + @property - def nickname(self): + def nickname(self) -> str: """ - The person's nicknames. Any text found inside of quotes (``""``) or + The person's nicknames. Any text found inside of quotes (``""``) or parenthesis (``()``) """ return " ".join(self.nickname_list) or self.C.empty_attribute_default - - ### setter methods - - def _set_list(self, attr, value): + + @nickname.setter + def nickname(self, value: str | list[str] | None) -> None: + self._set_list('nickname', value) + + @property + def surnames_list(self) -> list[str]: + """ + List of middle names followed by last name. + """ + return self.middle_list + self.last_list + + @property + def surnames(self) -> str: + """ + A string of all middle names followed by the last name. + """ + return " ".join(self.surnames_list) or self.C.empty_attribute_default + + # setter methods + + def _set_list(self, attr: str, value: str | list[str] | None) -> None: if isinstance(value, list): val = value - elif isinstance(value, text_types): + elif isinstance(value, (str, bytes)): val = [value] elif value is None: val = [] else: raise TypeError( - "Can only assign strings, lists or None to name attributes." - " Got {0}".format(type(value))) + "Can only assign strings, lists or None to name attributes." + " Got {}".format(type(value))) setattr(self, attr+"_list", self.parse_pieces(val)) - - @title.setter - def title(self, value): - self._set_list('title', value) - - @first.setter - def first(self, value): - self._set_list('first', value) - - @middle.setter - def middle(self, value): - self._set_list('middle', value) - - @last.setter - def last(self, value): - self._set_list('last', value) - - @suffix.setter - def suffix(self, value): - self._set_list('suffix', value) - - @nickname.setter - def nickname(self, value): - self._set_list('nickname', value) - - ### Parse helpers - - def is_title(self, value): + + # Parse helpers + def is_title(self, value: str) -> bool: """Is in the :py:data:`~nameparser.config.titles.TITLES` set.""" return lc(value) in self.C.titles - - def is_conjunction(self, piece): - """Is in the conjuctions set and not :py:func:`is_an_initial()`.""" - return piece.lower() in self.C.conjunctions and not self.is_an_initial(piece) - - def is_prefix(self, piece): + + def is_conjunction(self, piece: str) -> bool: + """Is in the conjunctions set and not :py:func:`is_an_initial()`.""" + if isinstance(piece, list): + for item in piece: + if self.is_conjunction(item): + return True + else: + return piece.lower() in self.C.conjunctions and not self.is_an_initial(piece) + + def is_prefix(self, piece: str) -> bool: """ - Lowercase and no periods version of piece is in the - `~nameparser.config.titles.PREFIXES` set. + Lowercase and no periods version of piece is in the + :py:data:`~nameparser.config.prefixes.PREFIXES` set. """ - return lc(piece) in self.C.prefixes + if isinstance(piece, list): + for item in piece: + if self.is_prefix(item): + return True + else: + return lc(piece) in self.C.prefixes - def is_roman_numeral(self, value): + def is_roman_numeral(self, value: str) -> bool: """ - Matches the ``roman_numeral`` regular expression in + Matches the ``roman_numeral`` regular expression in :py:data:`~nameparser.config.regexes.REGEXES`. """ return bool(self.C.regexes.roman_numeral.match(value)) - - def is_suffix(self, piece): + + def is_suffix(self, piece: str) -> bool: """ - Is in the suffixes set and not :py:func:`is_an_initial()`. - - Some suffixes may be acronyms (M.B.A) while some are not (Jr.), + Is in the suffixes set and not :py:func:`is_an_initial()`. + + Some suffixes may be acronyms (M.B.A) while some are not (Jr.), so we remove the periods from `piece` when testing against `C.suffix_acronyms`. """ # suffixes may have periods inside them like "M.D." - return ((lc(piece).replace('.','') in self.C.suffix_acronyms) \ - or (lc(piece) in self.C.suffix_not_acronyms)) \ - and not self.is_an_initial(piece) - - def are_suffixes(self, pieces): + if isinstance(piece, list): + for item in piece: + if self.is_suffix(item): + return True + else: + return ((lc(piece).replace('.', '') in self.C.suffix_acronyms) + or (lc(piece) in self.C.suffix_not_acronyms)) \ + and not self.is_an_initial(piece) + + def are_suffixes(self, pieces: Iterable[str]) -> bool: """Return True if all pieces are suffixes.""" for piece in pieces: if not self.is_suffix(piece): return False return True - - def is_rootname(self, piece): + + def is_rootname(self, piece: str) -> bool: """ Is not a known title, suffix or prefix. Just first, middle, last names. """ return lc(piece) not in self.C.suffixes_prefixes_titles \ - and not self.is_an_initial(piece) - - def is_an_initial(self, value): + and not self.is_an_initial(piece) + + def is_an_initial(self, value: str) -> bool: """ Words with a single period at the end, or a single uppercase letter. - - Matches the ``initial`` regular expression in + + Matches the ``initial`` regular expression in :py:data:`~nameparser.config.regexes.REGEXES`. """ return bool(self.C.regexes.initial.match(value)) - - ### full_name parser - + # full_name parser + @property - def full_name(self): - """The name string to be parsed.""" - return self._full_name - + def full_name(self) -> str: + """The string output of the HumanName instance.""" + return self.__str__() + @full_name.setter - def full_name(self, value): + def full_name(self, value: str | bytes) -> None: self.original = value - self._full_name = value - if isinstance(value, binary_type): + + if isinstance(value, bytes): self._full_name = value.decode(self.encoding) + else: + self._full_name = value + self.parse_full_name() - - def collapse_whitespace(self, string): + + def collapse_whitespace(self, string: str) -> str: # collapse multiple spaces into single space - return self.C.regexes.spaces.sub(" ", string.strip()) - - def pre_process(self): + string = self.C.regexes.spaces.sub(" ", string.strip()) + if string.endswith(","): + string = string[:-1] + return string + + def pre_process(self) -> None: """ - + This method happens at the beginning of the :py:func:`parse_full_name` before any other processing of the string aside from unicode normalization, so it's a good place to do any custom handling in a - subclass. Runs :py:func:`parse_nicknames`. - + subclass. Runs :py:func:`parse_nicknames` and :py:func:`squash_emoji`. + """ + self.fix_phd() self.parse_nicknames() self.squash_emoji() - def post_process(self): + def post_process(self) -> None: """ This happens at the end of the :py:func:`parse_full_name` after - all other processing has taken place. Runs :py:func:`handle_firstnames`. + all other processing has taken place. Runs :py:func:`handle_firstnames` + and :py:func:`handle_capitalization`. """ self.handle_firstnames() + self.handle_capitalization() - def parse_nicknames(self): + def fix_phd(self) -> None: + _re = self.C.regexes.phd + + if match := _re.search(self._full_name): + self.suffix_list.extend(match.groups()) + self._full_name = _re.sub('', self._full_name) + + def parse_nicknames(self) -> None: """ - The content of parenthesis or double quotes in the name will - be treated as nicknames. This happens before any other - processing of the name. + The content of parenthesis or quotes in the name will be added to the + nicknames list. This happens before any other processing of the name. + + Single quotes cannot span white space characters and must border + white space to allow for quotes in names like O'Connor and Kawai'ae'a. + Double quotes and parenthesis can span white space. + + Loops through 3 :py:data:`~nameparser.config.regexes.REGEXES`; + `quoted_word`, `double_quotes` and `parenthesis`. """ - # https://code.google.com/p/python-nameparser/issues/detail?id=33 - re_nickname = self.C.regexes.nickname - if re_nickname.search(self._full_name): - self.nickname_list = re_nickname.findall(self._full_name) - self._full_name = re_nickname.sub('', self._full_name) - def squash_emoji(self): + re_quoted_word = self.C.regexes.quoted_word + re_double_quotes = self.C.regexes.double_quotes + re_parenthesis = self.C.regexes.parenthesis + + for _re in (re_quoted_word, re_double_quotes, re_parenthesis): + if _re.search(self._full_name): + self.nickname_list += [x for x in _re.findall(self._full_name)] + self._full_name = _re.sub('', self._full_name) + + def squash_emoji(self) -> None: """ Remove emoji from the input string. """ @@ -402,32 +559,32 @@ def squash_emoji(self): if re_emoji and re_emoji.search(self._full_name): self._full_name = re_emoji.sub('', self._full_name) - def handle_firstnames(self): + def handle_firstnames(self) -> None: """ If there are only two parts and one is a title, assume it's a last name instead of a first name. e.g. Mr. Johnson. Unless it's a special title like "Sir", then when it's followed by a single name that name is always - a first name. + a first name. """ if self.title \ and len(self) == 2 \ - and not lc(self.title) in self.C.first_name_titles: + and lc(self.title) not in self.C.first_name_titles: self.last, self.first = self.first, self.last - def parse_full_name(self): + def parse_full_name(self) -> None: """ - + The main parse method for the parser. This method is run upon assignment to the :py:attr:`full_name` attribute or instantiation. Basic flow is to hand off to :py:func:`pre_process` to handle nicknames. It then splits on commas and chooses a code path depending on the number of commas. - + :py:func:`parse_pieces` then splits those parts on spaces and - :py:func:`join_on_conjunctions` joins any pieces next to conjunctions. + :py:func:`join_on_conjunctions` joins any pieces next to conjunctions. """ - + self.title_list = [] self.first_list = [] self.middle_list = [] @@ -435,23 +592,22 @@ def parse_full_name(self): self.suffix_list = [] self.nickname_list = [] self.unparsable = True - - + self.pre_process() - + self._full_name = self.collapse_whitespace(self._full_name) - + # break up full_name by commas parts = [x.strip() for x in self._full_name.split(",")] - - log.debug("full_name: {0}".format(self._full_name)) - log.debug("parts: {0}".format(parts)) - + + log.debug("full_name: %s", self._full_name) + log.debug("parts: %s", parts) + if len(parts) == 1: - + # no commas, title first middle middle middle last suffix # part[0] - + pieces = self.parse_pieces(parts) p_len = len(pieces) for i, piece in enumerate(pieces): @@ -459,56 +615,62 @@ def parse_full_name(self): nxt = pieces[i + 1] except IndexError: nxt = None - + # title must have a next piece, unless it's just a title - if self.is_title(piece) \ + if not self.first \ and (nxt or p_len == 1) \ - and not self.first: + and self.is_title(piece): self.title_list.append(piece) continue if not self.first: + if p_len == 1 and self.nickname: + self.last_list.append(piece) + continue self.first_list.append(piece) continue if self.are_suffixes(pieces[i+1:]) or \ - ( + ( # if the next piece is the last piece and a roman # numeral but this piece is not an initial - self.is_roman_numeral(nxt) and i == p_len - 2 + nxt is not None and \ + self.is_roman_numeral(nxt) and i == p_len - 2 and not self.is_an_initial(piece) - ): + ): self.last_list.append(piece) self.suffix_list += pieces[i+1:] break if not nxt: self.last_list.append(piece) continue - + self.middle_list.append(piece) else: # if all the end parts are suffixes and there is more than one piece # in the first part. (Suffixes will never appear after last names # only, and allows potential first names to be in suffixes, e.g. # "Johnson, Bart" + + post_comma_pieces = self.parse_pieces(parts[1].split(' '), 1) + if self.are_suffixes(parts[1].split(' ')) \ and len(parts[0].split(' ')) > 1: - - # suffix comma: + + # suffix comma: # title first middle last [suffix], suffix [suffix] [, suffix] # parts[0], parts[1:...] - - + self.suffix_list += parts[1:] pieces = self.parse_pieces(parts[0].split(' ')) - log.debug("pieces: {0}".format(u(pieces))) + log.debug("pieces: %s", str(pieces)) for i, piece in enumerate(pieces): try: nxt = pieces[i + 1] except IndexError: nxt = None - if self.is_title(piece) \ + if not self.first \ and (nxt or len(pieces) == 1) \ - and not self.first: + and self.is_title(piece): self.title_list.append(piece) continue if not self.first: @@ -523,33 +685,32 @@ def parse_full_name(self): continue self.middle_list.append(piece) else: - - # lastname comma: + + # lastname comma: # last [suffix], title first middles[,] suffix [,suffix] # parts[0], parts[1], parts[2:...] - pieces = self.parse_pieces(parts[1].split(' '), 1) - - log.debug("pieces: {0}".format(u(pieces))) - + + log.debug("post-comma pieces: %s", str(post_comma_pieces)) + # lastname part may have suffixes in it lastname_pieces = self.parse_pieces(parts[0].split(' '), 1) for piece in lastname_pieces: - # the first one is always a last name, even if it look like + # the first one is always a last name, even if it looks like # a suffix if self.is_suffix(piece) and len(self.last_list) > 0: self.suffix_list.append(piece) else: self.last_list.append(piece) - - for i, piece in enumerate(pieces): + + for i, piece in enumerate(post_comma_pieces): try: - nxt = pieces[i + 1] + nxt = post_comma_pieces[i + 1] except IndexError: nxt = None - - if self.is_title(piece) \ - and (nxt or len(pieces) == 1) \ - and not self.first: + + if not self.first \ + and (nxt or len(post_comma_pieces) == 1) \ + and self.is_title(piece): self.title_list.append(piece) continue if not self.first: @@ -564,50 +725,49 @@ def parse_full_name(self): self.suffix_list += parts[2:] except IndexError: pass - + if len(self) < 0: - log.info("Unparsable: \"{}\" ".format(self.original)) + log.info("Unparsable: \"%s\" ", self.original) else: self.unparsable = False self.post_process() - - def parse_pieces(self, parts, additional_parts_count=0): + def parse_pieces(self, parts: Iterable[str], additional_parts_count: int = 0) -> list[str]: """ Split parts on spaces and remove commas, join on conjunctions and lastname prefixes. If parts have periods in the middle, try splitting on periods and check if the parts are titles or suffixes. If they are add to the constant so they will be found. - + :param list parts: name part strings from the comma split - :param int additional_parts_count: - - if the comma format contains other parts, we need to know - how many there are to decide if things should be considered a + :param int additional_parts_count: + + if the comma format contains other parts, we need to know + how many there are to decide if things should be considered a conjunction. :return: pieces split on spaces and joined on conjunctions :rtype: list """ - - output = [] + + output: list[str] = [] for part in parts: - if not isinstance(part, text_types): + if not isinstance(part, (str, bytes)): raise TypeError("Name parts must be strings. " - "Got {0}".format(type(part))) + " Got {}".format(type(part))) output += [x.strip(' ,') for x in part.split(' ')] - + # If part contains periods, check if it's multiple titles or suffixes # together without spaces if so, add the new part with periods to the # constants so they get parsed correctly later for part in output: # if this part has a period not at the beginning or end - if self.C.regexes.period_not_at_end.match(part): + if self.C.regexes.period_not_at_end and self.C.regexes.period_not_at_end.match(part): # split on periods, any of the split pieces titles or suffixes? # ("Lt.Gov.") period_chunks = part.split(".") - titles = list(filter(self.is_title, period_chunks)) + titles = list(filter(self.is_title, period_chunks)) suffixes = list(filter(self.is_suffix, period_chunks)) - + # add the part to the constant so it will be found if len(list(titles)): self.C.titles.add(part) @@ -615,68 +775,55 @@ def parse_pieces(self, parts, additional_parts_count=0): if len(list(suffixes)): self.C.suffix_not_acronyms.add(part) continue - + return self.join_on_conjunctions(output, additional_parts_count) - - def join_on_conjunctions(self, pieces, additional_parts_count=0): + + def join_on_conjunctions(self, pieces: list[str], additional_parts_count: int = 0) -> list[str]: """ Join conjunctions to surrounding pieces. Title- and prefix-aware. e.g.: - + ['Mr.', 'and'. 'Mrs.', 'John', 'Doe'] ==> ['Mr. and Mrs.', 'John', 'Doe'] - + ['The', 'Secretary', 'of', 'State', 'Hillary', 'Clinton'] ==> ['The Secretary of State', 'Hillary', 'Clinton'] - + When joining titles, saves newly formed piece to the instance's titles constant so they will be parsed correctly later. E.g. after parsing the example names above, 'The Secretary of State' and 'Mr. and Mrs.' would be present in the titles constant set. - + :param list pieces: name pieces strings after split on spaces - :param int additional_parts_count: - :return: new list with piece next to conjunctions merged into one piece - with spaces in it. + :param int additional_parts_count: + :return: new list with piece next to conjunctions merged into one piece + with spaces in it. :rtype: list - + """ length = len(pieces) + additional_parts_count # don't join on conjunctions if there's only 2 parts if length < 3: return pieces - + rootname_pieces = [p for p in pieces if self.is_rootname(p)] total_length = len(rootname_pieces) + additional_parts_count - + # find all the conjunctions, join any conjunctions that are next to each # other, then join those newly joined conjunctions and any single # conjunctions to the piece before and after it - conj_index = [i for i, piece in enumerate(pieces) - if self.is_conjunction(piece)] - - contiguous_conj_i = [] - for i, val in enumerate(conj_index): - try: - if conj_index[i+1] == val+1: - contiguous_conj_i += [val] - except IndexError: - pass - + conj_index = [i for i, piece in enumerate(pieces) + if self.is_conjunction(piece)] + contiguous_conj_i = group_contiguous_integers(conj_index) - - delete_i = [] - for i in contiguous_conj_i: - if type(i) == tuple: - new_piece = " ".join(pieces[ i[0] : i[1]+1] ) - delete_i += list(range( i[0]+1, i[1]+1 )) - pieces[i[0]] = new_piece - else: - new_piece = " ".join(pieces[ i : i+2 ]) - delete_i += [i+1] - pieces[i] = new_piece - #add newly joined conjunctions to constants to be found later + + delete_i: list[int] = [] + for cont_i in contiguous_conj_i: + new_piece = " ".join(pieces[cont_i[0]: cont_i[1]+1]) + delete_i += list(range(cont_i[0]+1, cont_i[1]+1)) + pieces[cont_i[0]] = new_piece + # add newly joined conjunctions to constants to be found later self.C.conjunctions.add(new_piece) - + for i in reversed(delete_i): # delete pieces in reverse order or the index changes on each delete del pieces[i] @@ -687,7 +834,7 @@ def join_on_conjunctions(self, pieces, additional_parts_count=0): # refresh conjunction index locations conj_index = [i for i, piece in enumerate(pieces) if self.is_conjunction(piece)] - + for i in conj_index: if len(pieces[i]) == 1 and total_length < 4: # if there are only 3 total parts (minus known titles, suffixes @@ -695,8 +842,8 @@ def join_on_conjunctions(self, pieces, additional_parts_count=0): # treating it as an initial rather than a conjunction. # http://code.google.com/p/python-nameparser/issues/detail?id=11 continue - - if i is 0: + + if i == 0: new_piece = " ".join(pieces[i:i+2]) if self.is_title(pieces[i+1]): # when joining to a title, make new_piece a title too @@ -704,11 +851,11 @@ def join_on_conjunctions(self, pieces, additional_parts_count=0): pieces[i] = new_piece pieces.pop(i+1) # subtract 1 from the index of all the remaining conjunctions - for j,val in enumerate(conj_index): + for j, val in enumerate(conj_index): if val > i: - conj_index[j]=val-1 - - else: + conj_index[j] = val-1 + + else: new_piece = " ".join(pieces[i-1:i+2]) if self.is_title(pieces[i-1]): # when joining to a title, make new_piece a title too @@ -723,64 +870,96 @@ def join_on_conjunctions(self, pieces, additional_parts_count=0): # subtract the number of removed pieces from the index # of all the remaining conjunctions - for j,val in enumerate(conj_index): + for j, val in enumerate(conj_index): if val > i: conj_index[j] = val - rm_count - - + # join prefixes to following lastnames: ['de la Vega'], ['van Buren'] prefixes = list(filter(self.is_prefix, pieces)) if prefixes: - i = pieces.index(prefixes[0]) - # join everything after the prefix until the next suffix - next_suffix = list(filter(self.is_suffix, pieces[i:])) - if next_suffix: - j = pieces.index(next_suffix[0]) - new_piece = ' '.join(pieces[i:j]) - pieces = pieces[:i] + [new_piece] + pieces[j:] - else: - new_piece = ' '.join(pieces[i:]) - pieces = pieces[:i] + [new_piece] - - log.debug("pieces: {0}".format(pieces)) + for prefix in prefixes: + try: + i = pieces.index(prefix) + except ValueError: + # If the prefix is no longer in pieces, it's because it has been + # combined with the prefix that appears right before (or before that when + # chained together) in the last loop, so the index of that newly created + # piece is the same as in the last loop, i==i still, and we want to join + # it to the next piece. + pass + + new_piece = '' + + # join everything after the prefix until the next prefix or suffix + + try: + if i == 0 and total_length >= 1: + # If it's the first piece and there are more than 1 rootnames, assume it's a first name + continue + next_prefix = next(iter(filter(self.is_prefix, pieces[i + 1:]))) + j = pieces.index(next_prefix, i + 1) + if j == i + 1: + # if there are two prefixes in sequence, join to the following piece + j += 1 + new_piece = ' '.join(pieces[i:j]) + pieces = pieces[:i] + [new_piece] + pieces[j:] + except StopIteration: + try: + # if there are no more prefixes, look for a suffix to stop at + stop_at = next(iter(filter(self.is_suffix, pieces[i + 1:]))) + j = pieces.index(stop_at) + new_piece = ' '.join(pieces[i:j]) + pieces = pieces[:i] + [new_piece] + pieces[j:] + except StopIteration: + # if there were no suffixes, nothing to stop at so join all + # remaining pieces + new_piece = ' '.join(pieces[i:]) + pieces = pieces[:i] + [new_piece] + + log.debug("pieces: %s", pieces) return pieces - - - ### Capitalization Support - - def cap_word(self, word): - if self.is_prefix(word) or self.is_conjunction(word): + + # Capitalization Support + + def cap_word(self, word: str, attribute: HumanNameAttributeT) -> str: + if (self.is_prefix(word) and attribute in ('last', 'middle')) \ + or self.is_conjunction(word): return word.lower() exceptions = self.C.capitalization_exceptions if lc(word) in exceptions: return exceptions[lc(word)] mac_match = self.C.regexes.mac.match(word) if mac_match: - def cap_after_mac(m): + def cap_after_mac(m: re.Match) -> str: return m.group(1).capitalize() + m.group(2).capitalize() return self.C.regexes.mac.sub(cap_after_mac, word) else: return word.capitalize() - def cap_piece(self, piece): + def cap_piece(self, piece: str, attribute: HumanNameAttributeT) -> str: if not piece: return "" - replacement = lambda m: self.cap_word(m.group(0)) + + def replacement(m: re.Match) -> str: + return self.cap_word(m.group(0), attribute) + return self.C.regexes.word.sub(replacement, piece) - def capitalize(self, force=False): + def capitalize(self, force: bool | None = None) -> None: """ The HumanName class can try to guess the correct capitalization of name entered in all upper or lower case. By default, it will not adjust the case of names entered in mixed case. To run capitalization on all names pass the parameter `force=True`. - - :param bool force: force capitalization of strings that include mixed case + + :param bool force: Forces capitalization of mixed case strings. This + parameter overrides rules set within + :py:class:`~nameparser.config.CONSTANTS`. **Usage** - + .. doctest:: capitalize - + >>> name = HumanName('bob v. de la macdole-eisenhower phd') >>> name.capitalize() >>> str(name) @@ -788,18 +967,29 @@ def capitalize(self, force=False): >>> # Don't touch good names >>> name = HumanName('Shirley Maclaine') >>> name.capitalize() - >>> str(name) + >>> str(name) 'Shirley Maclaine' >>> name.capitalize(force=True) - >>> str(name) + >>> str(name) 'Shirley MacLaine' - + """ - name = u(self) + name = str(self) + force = self.C.force_mixed_case_capitalization \ + if force is None else force + if not force and not (name == name.upper() or name == name.lower()): return - self.title_list = self.cap_piece(self.title ).split(' ') - self.first_list = self.cap_piece(self.first ).split(' ') - self.middle_list = self.cap_piece(self.middle).split(' ') - self.last_list = self.cap_piece(self.last ).split(' ') - self.suffix_list = self.cap_piece(self.suffix).split(', ') + self.title_list = self.cap_piece(self.title, 'title').split(' ') + self.first_list = self.cap_piece(self.first, 'first').split(' ') + self.middle_list = self.cap_piece(self.middle, 'middle').split(' ') + self.last_list = self.cap_piece(self.last, 'last').split(' ') + self.suffix_list = self.cap_piece(self.suffix, 'suffix').split(', ') + + def handle_capitalization(self) -> None: + """ + Handles capitalization configurations set within + :py:class:`~nameparser.config.CONSTANTS`. + """ + if self.C.capitalize_name: + self.capitalize() diff --git a/nameparser/py.typed b/nameparser/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/nameparser/util.py b/nameparser/util.py index 4ef7458..3b28fe7 100644 --- a/nameparser/util.py +++ b/nameparser/util.py @@ -1,38 +1,16 @@ import logging +from typing import Literal # http://code.google.com/p/python-nameparser/issues/detail?id=10 log = logging.getLogger('HumanName') -try: - log.addHandler(logging.NullHandler()) -except AttributeError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - log.addHandler(NullHandler()) +log.addHandler(logging.NullHandler()) log.setLevel(logging.ERROR) -import sys -if sys.version_info[0] < 3: +HumanNameAttributeT = Literal['title', 'first', 'middle', 'last', 'suffix', 'nickname', 'surnames'] - text_type = unicode - binary_type = str - def u(x, encoding=None): - if encoding: - return unicode(x, encoding) - else: - return unicode(x) - -else: - text_type = str - binary_type = bytes - - def u(x, encoding=None): - return text_type(x) - -text_types = (text_type, binary_type) -def lc(value): +def lc(value: str) -> str: """Lower case and remove any periods to normalize for comparison.""" if not value: return '' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4355287 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[project] +name = "nameparser" +description = "A simple Python module for parsing human names into their individual components." +readme = "README.rst" +requires-python = ">=3.10" +license = {text = "LGPL"} +authors = [{name = "Derek Gulbranson", email = "derek73@gmail.com"}] +dynamic = ["version"] +keywords = ["names", "parser"] +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Development Status :: 5 - Production/Stable", + "Natural Language :: English", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Linguistic", +] +dependencies = [ + "typing_extensions (>=4.5.0); python_version < '3.11'" +] + +[build-system] +requires = ["setuptools (>=82.0.1)"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "nameparser._version.__version__"} + +[dependency-groups] +dev = [ + "dill (>=0.2.5)", + "sphinx (>=8)", + "mypy (>=2.1)", + "ruff (>=0.15)" +] + +[tool.mypy] +packages = ["nameparser"] + +[[tool.mypy.overrides]] +module = [ + "dill.*", +] +ignore_missing_imports = true + +[tool.ruff.lint] +extend-select = [ + "ANN", # flake8-annotations + "UP", # pyupgrade +] +ignore = [ + "E74", # ambiguous-{variable,class,function}-name (I have trouble believing that these are real rules) + "UP031", # printf-string-formatting (the code already uses % formatting) + "UP032", # f-string (the code already uses str.format) +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100755 index 4986703..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -try: - from setuptools import setup -except ImportError: - from distutils.core import setup -import nameparser -import os - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - -README = read('README.rst') - -setup(name='nameparser', - packages = ['nameparser','nameparser.config'], - description = 'A simple Python module for parsing human names into their individual components.', - long_description = README, - version = nameparser.__version__, - url = nameparser.__url__, - author = nameparser.__author__, - author_email = nameparser.__author_email__, - license = nameparser.__license__, - keywords = ['names','parser'], - classifiers = [ - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Development Status :: 5 - Production/Stable', - 'Natural Language :: English', - "Topic :: Software Development :: Libraries :: Python Modules", - 'Topic :: Text Processing :: Linguistic', - ] - ) diff --git a/tests.py b/tests.py index 31a7c19..c137ab4 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +# ruff: noqa: E402 +import unittest """ Run this file to run the tests. @@ -19,79 +19,75 @@ """ import logging +import re +from typing import Generic, TypeVar + try: import dill except ImportError: dill = False from nameparser import HumanName -from nameparser.util import u -from nameparser.config import Constants +from nameparser.config import Constants, TupleManager log = logging.getLogger('HumanName') -import unittest -try: - unittest.expectedFailure -except AttributeError: - # Python 2.6 backport - import unittest2 as unittest + +T = TypeVar('T') -class HumanNameTestBase(unittest.TestCase): - def m(self, actual, expected, hn): - """assertEquals with a better message and awareness of hn.C.empty_attribute_default""" - expected = expected or hn.C.empty_attribute_default +class HumanNameTestBase(unittest.TestCase, Generic[T]): + def m(self, actual: T, expected: T, hn: HumanName) -> None: + """assertEqual with a better message and awareness of hn.C.empty_attribute_default""" + expected_ = expected or hn.C.empty_attribute_default try: - self.assertEqual(actual, expected, "'%s' != '%s' for '%s'\n%r" % ( + self.assertEqual(actual, expected_, "'%s' != '%s' for '%s'\n%r" % ( actual, expected, - hn.full_name, + hn.original, hn )) except UnicodeDecodeError: - self.assertEquals(actual, expected) - + self.assertEqual(actual, expected_) class HumanNamePythonTests(HumanNameTestBase): - def test_utf8(self): + def test_utf8(self) -> None: hn = HumanName("de la Véña, Jüan") self.m(hn.first, "Jüan", hn) self.m(hn.last, "de la Véña", hn) - def test_string_output(self): + def test_string_output(self) -> None: hn = HumanName("de la Véña, Jüan") - print(hn) - print(repr(hn)) + self.m(str(hn), "Jüan de la Véña", hn) - def test_escaped_utf8_bytes(self): + def test_escaped_utf8_bytes(self) -> None: hn = HumanName(b'B\xc3\xb6ck, Gerald') self.m(hn.first, "Gerald", hn) self.m(hn.last, "Böck", hn) - def test_len(self): + def test_len(self) -> None: hn = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") self.m(len(hn), 5, hn) hn = HumanName("John Doe") self.m(len(hn), 2, hn) - @unittest.skipUnless(dill,"requires python-dill module to test pickling") - def test_config_pickle(self): - C = Constants() - self.assertTrue(dill.pickles(C)) + @unittest.skipUnless(dill, "requires python-dill module to test pickling") + def test_config_pickle(self) -> None: + constants = Constants() + self.assertTrue(dill.pickles(constants)) - @unittest.skipUnless(dill,"requires python-dill module to test pickling") - def test_name_instance_pickle(self): + @unittest.skipUnless(dill, "requires python-dill module to test pickling") + def test_name_instance_pickle(self) -> None: hn = HumanName("Title First Middle Middle Last, Jr.") self.assertTrue(dill.pickles(hn)) - def test_comparison(self): + def test_comparison(self) -> None: hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") hn2 = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC") self.assertTrue(hn1 == hn2) - self.assertTrue(not hn1 is hn2) + self.assertTrue(hn1 is not hn2) self.assertTrue(hn1 == "Dr. John P. Doe-Ray CLU, CFP, LUTC") hn1 = HumanName("Doe, Dr. John P., CLU, CFP, LUTC") hn2 = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC") @@ -101,7 +97,7 @@ def test_comparison(self): self.assertTrue(not hn1 == ["test"]) self.assertTrue(not hn1 == {"test": hn2}) - def test_assignment_to_full_name(self): + def test_assignment_to_full_name(self) -> None: hn = HumanName("John A. Kenneth Doe, Jr.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) @@ -112,7 +108,12 @@ def test_assignment_to_full_name(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test_assignment_to_attribute(self): + def test_get_full_name_attribute_references_internal_lists(self) -> None: + hn = HumanName("John Williams") + hn.first_list = ["Larry"] + self.m(hn.full_name, "Larry Williams", hn) + + def test_assignment_to_attribute(self) -> None: hn = HumanName("John A. Kenneth Doe, Jr.") hn.last = "de la Vega" self.m(hn.last, "de la Vega", hn) @@ -127,35 +128,35 @@ def test_assignment_to_attribute(self): with self.assertRaises(TypeError): hn.suffix = [['test']] with self.assertRaises(TypeError): - hn.suffix = {"test":"test"} + hn.suffix = {"test": "test"} - def test_assign_list_to_attribute(self): + def test_assign_list_to_attribute(self) -> None: hn = HumanName("John A. Kenneth Doe, Jr.") - hn.title = ["test1","test2"] + hn.title = ["test1", "test2"] self.m(hn.title, "test1 test2", hn) - hn.first = ["test3","test4"] + hn.first = ["test3", "test4"] self.m(hn.first, "test3 test4", hn) - hn.middle = ["test5","test6","test7"] + hn.middle = ["test5", "test6", "test7"] self.m(hn.middle, "test5 test6 test7", hn) - hn.last = ["test8","test9","test10"] + hn.last = ["test8", "test9", "test10"] self.m(hn.last, "test8 test9 test10", hn) hn.suffix = ['test'] self.m(hn.suffix, "test", hn) - def test_comparison_case_insensitive(self): + def test_comparison_case_insensitive(self) -> None: hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") hn2 = HumanName("dr. john p. doe-Ray, CLU, CFP, LUTC") self.assertTrue(hn1 == hn2) - self.assertTrue(not hn1 is hn2) + self.assertTrue(hn1 is not hn2) self.assertTrue(hn1 == "Dr. John P. Doe-ray clu, CFP, LUTC") - def test_slice(self): + def test_slice(self) -> None: hn = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") self.m(list(hn), ['Dr.', 'John', 'P.', 'Doe-Ray', 'CLU, CFP, LUTC'], hn) - self.m(hn[1:], ['John', 'P.', 'Doe-Ray', 'CLU, CFP, LUTC',hn.C.empty_attribute_default], hn) + self.m(hn[1:], ['John', 'P.', 'Doe-Ray', 'CLU, CFP, LUTC', hn.C.empty_attribute_default], hn) self.m(hn[1:-2], ['John', 'P.', 'Doe-Ray'], hn) - def test_getitem(self): + def test_getitem(self) -> None: hn = HumanName("Dr. John A. Kenneth Doe, Jr.") self.m(hn['title'], "Dr.", hn) self.m(hn['first'], "John", hn) @@ -163,261 +164,334 @@ def test_getitem(self): self.m(hn['middle'], "A. Kenneth", hn) self.m(hn['suffix'], "Jr.", hn) - def test_setitem(self): + def test_setitem(self) -> None: hn = HumanName("Dr. John A. Kenneth Doe, Jr.") hn['title'] = 'test' self.m(hn['title'], "test", hn) - hn['last'] = ['test','test2'] + hn['last'] = ['test', 'test2'] self.m(hn['last'], "test test2", hn) with self.assertRaises(TypeError): hn["suffix"] = [['test']] with self.assertRaises(TypeError): - hn["suffix"] = {"test":"test"} + hn["suffix"] = {"test": "test"} - def test_conjunction_names(self): + def test_conjunction_names(self) -> None: hn = HumanName("johnny y") self.m(hn.first, "johnny", hn) self.m(hn.last, "y", hn) - def test_prefix_names(self): + def test_prefix_names(self) -> None: hn = HumanName("vai la") self.m(hn.first, "vai", hn) self.m(hn.last, "la", hn) - def test_blank_name(self): + def test_blank_name(self) -> None: hn = HumanName() self.m(hn.first, "", hn) self.m(hn.last, "", hn) + def test_surnames_list_attribute(self) -> None: + hn = HumanName("John Edgar Casey Williams III") + self.m(hn.surnames_list, ["Edgar", "Casey", "Williams"], hn) + + def test_surnames_attribute(self) -> None: + hn = HumanName("John Edgar Casey Williams III") + self.m(hn.surnames, "Edgar Casey Williams", hn) + + def test_is_prefix_with_list(self) -> None: + hn = HumanName() + items = ['firstname', 'lastname', 'del'] + self.assertTrue(hn.is_prefix(items)) + self.assertTrue(hn.is_prefix(items[1:])) + + def test_is_conjunction_with_list(self) -> None: + hn = HumanName() + items = ['firstname', 'lastname', 'and'] + self.assertTrue(hn.is_conjunction(items)) + self.assertTrue(hn.is_conjunction(items[1:])) + + def test_override_constants(self) -> None: + C = Constants() + hn = HumanName(constants=C) + self.assertTrue(hn.C is C) + + def test_override_regex(self) -> None: + var = TupleManager([("spaces", re.compile(r"\s+", re.U)),]) + C = Constants(regexes=var) + hn = HumanName(constants=C) + self.assertTrue(hn.C.regexes == var) + + def test_override_titles(self) -> None: + var = ["abc","def"] + C = Constants(titles=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.titles) == sorted(var)) + + def test_override_first_name_titles(self) -> None: + var = ["abc","def"] + C = Constants(first_name_titles=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.first_name_titles) == sorted(var)) + + def test_override_prefixes(self) -> None: + var = ["abc","def"] + C = Constants(prefixes=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.prefixes) == sorted(var)) + + def test_override_suffix_acronyms(self) -> None: + var = ["abc","def"] + C = Constants(suffix_acronyms=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.suffix_acronyms) == sorted(var)) + + def test_override_suffix_not_acronyms(self) -> None: + var = ["abc","def"] + C = Constants(suffix_not_acronyms=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.suffix_not_acronyms) == sorted(var)) + + def test_override_conjunctions(self) -> None: + var = ["abc","def"] + C = Constants(conjunctions=var) + hn = HumanName(constants=C) + self.assertTrue(sorted(hn.C.conjunctions) == sorted(var)) + + def test_override_capitalization_exceptions(self) -> None: + var = TupleManager([("spaces", re.compile(r"\s+", re.U)),]) + C = Constants(capitalization_exceptions=var) + hn = HumanName(constants=C) + self.assertTrue(hn.C.capitalization_exceptions == var) + class FirstNameHandlingTests(HumanNameTestBase): - def test_first_name(self): + def test_first_name(self) -> None: hn = HumanName("Andrew") self.m(hn.first, "Andrew", hn) - def test_assume_title_and_one_other_name_is_last_name(self): + def test_assume_title_and_one_other_name_is_last_name(self) -> None: hn = HumanName("Rev Andrews") self.m(hn.title, "Rev", hn) self.m(hn.last, "Andrews", hn) - + # TODO: Seems "Andrews, M.D.", Andrews should be treated as a last name - # but other suffixes like "George Jr." should be first names. Might be + # but other suffixes like "George Jr." should be first names. Might be # related to https://github.com/derek73/python-nameparser/issues/2 @unittest.expectedFailure - def test_assume_suffix_title_and_one_other_name_is_last_name(self): + def test_assume_suffix_title_and_one_other_name_is_last_name(self) -> None: hn = HumanName("Andrews, M.D.") self.m(hn.suffix, "M.D.", hn) self.m(hn.last, "Andrews", hn) - - def test_suffix_in_lastname_part_of_lastname_comma_format(self): + + def test_suffix_in_lastname_part_of_lastname_comma_format(self) -> None: hn = HumanName("Smith Jr., John") self.m(hn.last, "Smith", hn) self.m(hn.first, "John", hn) self.m(hn.suffix, "Jr.", hn) - def test_sir_exception_to_first_name_rule(self): + def test_sir_exception_to_first_name_rule(self) -> None: hn = HumanName("Sir Gerald") self.m(hn.title, "Sir", hn) self.m(hn.first, "Gerald", hn) - - def test_king_exception_to_first_name_rule(self): + + def test_king_exception_to_first_name_rule(self) -> None: hn = HumanName("King Henry") self.m(hn.title, "King", hn) self.m(hn.first, "Henry", hn) - - def test_queen_exception_to_first_name_rule(self): + + def test_queen_exception_to_first_name_rule(self) -> None: hn = HumanName("Queen Elizabeth") self.m(hn.title, "Queen", hn) self.m(hn.first, "Elizabeth", hn) - - def test_dame_exception_to_first_name_rule(self): + + def test_dame_exception_to_first_name_rule(self) -> None: hn = HumanName("Dame Mary") self.m(hn.title, "Dame", hn) self.m(hn.first, "Mary", hn) - - def test_first_name_is_not_prefix_if_only_two_parts(self): + + def test_first_name_is_not_prefix_if_only_two_parts(self) -> None: """When there are only two parts, don't join prefixes or conjunctions""" hn = HumanName("Van Nguyen") self.m(hn.first, "Van", hn) self.m(hn.last, "Nguyen", hn) - def test_first_name_is_not_prefix_if_only_two_parts_comma(self): + def test_first_name_is_not_prefix_if_only_two_parts_comma(self) -> None: hn = HumanName("Nguyen, Van") self.m(hn.first, "Van", hn) self.m(hn.last, "Nguyen", hn) @unittest.expectedFailure - def test_first_name_is_prefix_if_three_parts(self): + def test_first_name_is_prefix_if_three_parts(self) -> None: """Not sure how to fix this without breaking Mr and Mrs""" hn = HumanName("Mr. Van Nguyen") self.m(hn.first, "Van", hn) self.m(hn.last, "Nguyen", hn) - + class HumanNameBruteForceTests(HumanNameTestBase): - def test1(self): + def test1(self) -> None: hn = HumanName("John Doe") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test2(self): + def test2(self) -> None: hn = HumanName("John Doe, Jr.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test3(self): + def test3(self) -> None: hn = HumanName("John Doe III") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test4(self): + def test4(self) -> None: hn = HumanName("Doe, John") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test5(self): + def test5(self) -> None: hn = HumanName("Doe, John, Jr.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test6(self): + def test6(self) -> None: hn = HumanName("Doe, John III") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test7(self): + def test7(self) -> None: hn = HumanName("John A. Doe") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) - def test8(self): + def test8(self) -> None: hn = HumanName("John A. Doe, Jr") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) self.m(hn.suffix, "Jr", hn) - def test9(self): + def test9(self) -> None: hn = HumanName("John A. Doe III") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) self.m(hn.suffix, "III", hn) - def test10(self): + def test10(self) -> None: hn = HumanName("Doe, John A.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) - def test11(self): + def test11(self) -> None: hn = HumanName("Doe, John A., Jr.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) self.m(hn.suffix, "Jr.", hn) - def test12(self): + def test12(self) -> None: hn = HumanName("Doe, John A., III") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) self.m(hn.suffix, "III", hn) - def test13(self): + def test13(self) -> None: hn = HumanName("John A. Kenneth Doe") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) - def test14(self): + def test14(self) -> None: hn = HumanName("John A. Kenneth Doe, Jr.") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "Jr.", hn) - def test15(self): + def test15(self) -> None: hn = HumanName("John A. Kenneth Doe III") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "III", hn) - def test16(self): + def test16(self) -> None: hn = HumanName("Doe, John. A. Kenneth") self.m(hn.first, "John.", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) - def test17(self): + def test17(self) -> None: hn = HumanName("Doe, John. A. Kenneth, Jr.") self.m(hn.first, "John.", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "Jr.", hn) - def test18(self): + def test18(self) -> None: hn = HumanName("Doe, John. A. Kenneth III") self.m(hn.first, "John.", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "III", hn) - def test19(self): + def test19(self) -> None: hn = HumanName("Dr. John Doe") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.title, "Dr.", hn) - def test20(self): + def test20(self) -> None: hn = HumanName("Dr. John Doe, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test21(self): + def test21(self) -> None: hn = HumanName("Dr. John Doe III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test22(self): + def test22(self) -> None: hn = HumanName("Doe, Dr. John") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test23(self): + def test23(self) -> None: hn = HumanName("Doe, Dr. John, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test24(self): + def test24(self) -> None: hn = HumanName("Doe, Dr. John III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test25(self): + def test25(self) -> None: hn = HumanName("Dr. John A. Doe") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) - def test26(self): + def test26(self) -> None: hn = HumanName("Dr. John A. Doe, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) @@ -425,7 +499,7 @@ def test26(self): self.m(hn.middle, "A.", hn) self.m(hn.suffix, "Jr.", hn) - def test27(self): + def test27(self) -> None: hn = HumanName("Dr. John A. Doe III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) @@ -433,14 +507,14 @@ def test27(self): self.m(hn.middle, "A.", hn) self.m(hn.suffix, "III", hn) - def test28(self): + def test28(self) -> None: hn = HumanName("Doe, Dr. John A.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.middle, "A.", hn) - def test29(self): + def test29(self) -> None: hn = HumanName("Doe, Dr. John A. Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "John", hn) @@ -448,7 +522,7 @@ def test29(self): self.m(hn.middle, "A.", hn) self.m(hn.suffix, "Jr.", hn) - def test30(self): + def test30(self) -> None: hn = HumanName("Doe, Dr. John A. III") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A.", hn) @@ -456,14 +530,14 @@ def test30(self): self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test31(self): + def test31(self) -> None: hn = HumanName("Dr. John A. Kenneth Doe") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test32(self): + def test32(self) -> None: hn = HumanName("Dr. John A. Kenneth Doe, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) @@ -471,14 +545,14 @@ def test32(self): self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test33(self): + def test33(self) -> None: hn = HumanName("Al Arnold Gore, Jr.") self.m(hn.middle, "Arnold", hn) self.m(hn.first, "Al", hn) self.m(hn.last, "Gore", hn) self.m(hn.suffix, "Jr.", hn) - def test34(self): + def test34(self) -> None: hn = HumanName("Dr. John A. Kenneth Doe III") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) @@ -486,14 +560,14 @@ def test34(self): self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test35(self): + def test35(self) -> None: hn = HumanName("Doe, Dr. John A. Kenneth") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test36(self): + def test36(self) -> None: hn = HumanName("Doe, Dr. John A. Kenneth Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) @@ -501,7 +575,7 @@ def test36(self): self.m(hn.last, "Doe", hn) self.m(hn.suffix, "Jr.", hn) - def test37(self): + def test37(self) -> None: hn = HumanName("Doe, Dr. John A. Kenneth III") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "A. Kenneth", hn) @@ -509,242 +583,242 @@ def test37(self): self.m(hn.last, "Doe", hn) self.m(hn.suffix, "III", hn) - def test38(self): + def test38(self) -> None: hn = HumanName("Juan de la Vega") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) - def test39(self): + def test39(self) -> None: hn = HumanName("Juan de la Vega, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "Jr.", hn) - def test40(self): + def test40(self) -> None: hn = HumanName("Juan de la Vega III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "III", hn) - def test41(self): + def test41(self) -> None: hn = HumanName("de la Vega, Juan") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) - def test42(self): + def test42(self) -> None: hn = HumanName("de la Vega, Juan, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "Jr.", hn) - def test43(self): + def test43(self) -> None: hn = HumanName("de la Vega, Juan III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "III", hn) - def test44(self): + def test44(self) -> None: hn = HumanName("Juan Velasquez y Garcia") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test45(self): + def test45(self) -> None: hn = HumanName("Juan Velasquez y Garcia, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test46(self): + def test46(self) -> None: hn = HumanName("Juan Velasquez y Garcia III") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test47(self): + def test47(self) -> None: hn = HumanName("Velasquez y Garcia, Juan") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test48(self): + def test48(self) -> None: hn = HumanName("Velasquez y Garcia, Juan, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test49(self): + def test49(self) -> None: hn = HumanName("Velasquez y Garcia, Juan III") self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test50(self): + def test50(self) -> None: hn = HumanName("Dr. Juan de la Vega") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) - def test51(self): + def test51(self) -> None: hn = HumanName("Dr. Juan de la Vega, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "Jr.", hn) - def test52(self): + def test52(self) -> None: hn = HumanName("Dr. Juan de la Vega III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "III", hn) - def test53(self): + def test53(self) -> None: hn = HumanName("de la Vega, Dr. Juan") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) - def test54(self): + def test54(self) -> None: hn = HumanName("de la Vega, Dr. Juan, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "Jr.", hn) - def test55(self): + def test55(self) -> None: hn = HumanName("de la Vega, Dr. Juan III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "III", hn) - def test56(self): + def test56(self) -> None: hn = HumanName("Dr. Juan Velasquez y Garcia") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test57(self): + def test57(self) -> None: hn = HumanName("Dr. Juan Velasquez y Garcia, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test58(self): + def test58(self) -> None: hn = HumanName("Dr. Juan Velasquez y Garcia III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test59(self): + def test59(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test60(self): + def test60(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test61(self): + def test61(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan III") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test62(self): + def test62(self) -> None: hn = HumanName("Juan Q. de la Vega") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q.", hn) self.m(hn.last, "de la Vega", hn) - def test63(self): + def test63(self) -> None: hn = HumanName("Juan Q. de la Vega, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q.", hn) self.m(hn.suffix, "Jr.", hn) - def test64(self): + def test64(self) -> None: hn = HumanName("Juan Q. de la Vega III") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q.", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.suffix, "III", hn) - def test65(self): + def test65(self) -> None: hn = HumanName("de la Vega, Juan Q.") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q.", hn) self.m(hn.last, "de la Vega", hn) - def test66(self): + def test66(self) -> None: hn = HumanName("de la Vega, Juan Q., Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q.", hn) self.m(hn.suffix, "Jr.", hn) - def test67(self): + def test67(self) -> None: hn = HumanName("de la Vega, Juan Q. III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q.", hn) self.m(hn.suffix, "III", hn) - def test68(self): + def test68(self) -> None: hn = HumanName("Juan Q. Velasquez y Garcia") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test69(self): + def test69(self) -> None: hn = HumanName("Juan Q. Velasquez y Garcia, Jr.") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test70(self): + def test70(self) -> None: hn = HumanName("Juan Q. Velasquez y Garcia III") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test71(self): + def test71(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q.") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test72(self): + def test72(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q., Jr.") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test73(self): + def test73(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q. III") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test74(self): + def test74(self) -> None: hn = HumanName("Dr. Juan Q. de la Vega") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q.", hn) self.m(hn.last, "de la Vega", hn) - def test75(self): + def test75(self) -> None: hn = HumanName("Dr. Juan Q. de la Vega, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -752,7 +826,7 @@ def test75(self): self.m(hn.title, "Dr.", hn) self.m(hn.suffix, "Jr.", hn) - def test76(self): + def test76(self) -> None: hn = HumanName("Dr. Juan Q. de la Vega III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -760,14 +834,14 @@ def test76(self): self.m(hn.title, "Dr.", hn) self.m(hn.suffix, "III", hn) - def test77(self): + def test77(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q.") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q.", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.title, "Dr.", hn) - def test78(self): + def test78(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q., Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -775,7 +849,7 @@ def test78(self): self.m(hn.suffix, "Jr.", hn) self.m(hn.title, "Dr.", hn) - def test79(self): + def test79(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q. III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -783,14 +857,14 @@ def test79(self): self.m(hn.suffix, "III", hn) self.m(hn.title, "Dr.", hn) - def test80(self): + def test80(self) -> None: hn = HumanName("Dr. Juan Q. Velasquez y Garcia") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test81(self): + def test81(self) -> None: hn = HumanName("Dr. Juan Q. Velasquez y Garcia, Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q.", hn) @@ -798,7 +872,7 @@ def test81(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test82(self): + def test82(self) -> None: hn = HumanName("Dr. Juan Q. Velasquez y Garcia III") self.m(hn.middle, "Q.", hn) self.m(hn.title, "Dr.", hn) @@ -806,14 +880,14 @@ def test82(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test83(self): + def test83(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q.") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test84(self): + def test84(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q., Jr.") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) @@ -821,7 +895,7 @@ def test84(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test85(self): + def test85(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q. III") self.m(hn.middle, "Q.", hn) self.m(hn.first, "Juan", hn) @@ -829,54 +903,54 @@ def test85(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test86(self): + def test86(self) -> None: hn = HumanName("Juan Q. Xavier de la Vega") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.last, "de la Vega", hn) - def test87(self): + def test87(self) -> None: hn = HumanName("Juan Q. Xavier de la Vega, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "Jr.", hn) - def test88(self): + def test88(self) -> None: hn = HumanName("Juan Q. Xavier de la Vega III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "III", hn) - def test89(self): + def test89(self) -> None: hn = HumanName("de la Vega, Juan Q. Xavier") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.last, "de la Vega", hn) - def test90(self): + def test90(self) -> None: hn = HumanName("de la Vega, Juan Q. Xavier, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "Jr.", hn) - def test91(self): + def test91(self) -> None: hn = HumanName("de la Vega, Juan Q. Xavier III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "III", hn) - def test92(self): + def test92(self) -> None: hn = HumanName("Dr. Juan Q. Xavier de la Vega") self.m(hn.first, "Juan", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.title, "Dr.", hn) self.m(hn.last, "de la Vega", hn) - def test93(self): + def test93(self) -> None: hn = HumanName("Dr. Juan Q. Xavier de la Vega, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -884,7 +958,7 @@ def test93(self): self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "Jr.", hn) - def test94(self): + def test94(self) -> None: hn = HumanName("Dr. Juan Q. Xavier de la Vega III") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -892,14 +966,14 @@ def test94(self): self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "III", hn) - def test95(self): + def test95(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q. Xavier") self.m(hn.first, "Juan", hn) self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.last, "de la Vega", hn) - def test96(self): + def test96(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q. Xavier, Jr.") self.m(hn.first, "Juan", hn) self.m(hn.last, "de la Vega", hn) @@ -907,7 +981,7 @@ def test96(self): self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "Jr.", hn) - def test97(self): + def test97(self) -> None: hn = HumanName("de la Vega, Dr. Juan Q. Xavier III") self.m(hn.first, "Juan", hn) self.m(hn.title, "Dr.", hn) @@ -915,54 +989,54 @@ def test97(self): self.m(hn.middle, "Q. Xavier", hn) self.m(hn.suffix, "III", hn) - def test98(self): + def test98(self) -> None: hn = HumanName("Juan Q. Xavier Velasquez y Garcia") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test99(self): + def test99(self) -> None: hn = HumanName("Juan Q. Xavier Velasquez y Garcia, Jr.") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test100(self): + def test100(self) -> None: hn = HumanName("Juan Q. Xavier Velasquez y Garcia III") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test101(self): + def test101(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q. Xavier") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test102(self): + def test102(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q. Xavier, Jr.") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test103(self): + def test103(self) -> None: hn = HumanName("Velasquez y Garcia, Juan Q. Xavier III") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test104(self): + def test104(self) -> None: hn = HumanName("Dr. Juan Q. Xavier Velasquez y Garcia") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test105(self): + def test105(self) -> None: hn = HumanName("Dr. Juan Q. Xavier Velasquez y Garcia, Jr.") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) @@ -970,7 +1044,7 @@ def test105(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test106(self): + def test106(self) -> None: hn = HumanName("Dr. Juan Q. Xavier Velasquez y Garcia III") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) @@ -978,14 +1052,14 @@ def test106(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test107(self): + def test107(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q. Xavier") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) self.m(hn.last, "Velasquez y Garcia", hn) - def test108(self): + def test108(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q. Xavier, Jr.") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) @@ -993,7 +1067,7 @@ def test108(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "Jr.", hn) - def test109(self): + def test109(self) -> None: hn = HumanName("Velasquez y Garcia, Dr. Juan Q. Xavier III") self.m(hn.middle, "Q. Xavier", hn) self.m(hn.first, "Juan", hn) @@ -1001,20 +1075,20 @@ def test109(self): self.m(hn.last, "Velasquez y Garcia", hn) self.m(hn.suffix, "III", hn) - def test110(self): + def test110(self) -> None: hn = HumanName("John Doe, CLU, CFP, LUTC") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "CLU, CFP, LUTC", hn) - def test111(self): + def test111(self) -> None: hn = HumanName("John P. Doe, CLU, CFP, LUTC") self.m(hn.first, "John", hn) self.m(hn.middle, "P.", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "CLU, CFP, LUTC", hn) - def test112(self): + def test112(self) -> None: hn = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC") self.m(hn.first, "John", hn) self.m(hn.middle, "P.", hn) @@ -1022,7 +1096,7 @@ def test112(self): self.m(hn.title, "Dr.", hn) self.m(hn.suffix, "CLU, CFP, LUTC", hn) - def test113(self): + def test113(self) -> None: hn = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") self.m(hn.title, "Dr.", hn) self.m(hn.middle, "P.", hn) @@ -1030,14 +1104,14 @@ def test113(self): self.m(hn.last, "Doe-Ray", hn) self.m(hn.suffix, "CLU, CFP, LUTC", hn) - def test115(self): + def test115(self) -> None: hn = HumanName("Hon. Barrington P. Doe-Ray, Jr.") self.m(hn.title, "Hon.", hn) self.m(hn.middle, "P.", hn) self.m(hn.first, "Barrington", hn) self.m(hn.last, "Doe-Ray", hn) - def test116(self): + def test116(self) -> None: hn = HumanName("Doe-Ray, Hon. Barrington P. Jr., CFP, LUTC") self.m(hn.title, "Hon.", hn) self.m(hn.middle, "P.", hn) @@ -1045,14 +1119,14 @@ def test116(self): self.m(hn.last, "Doe-Ray", hn) self.m(hn.suffix, "Jr., CFP, LUTC", hn) - def test117(self): + def test117(self) -> None: hn = HumanName("Rt. Hon. Paul E. Mary") self.m(hn.title, "Rt. Hon.", hn) self.m(hn.first, "Paul", hn) self.m(hn.middle, "E.", hn) self.m(hn.last, "Mary", hn) - def test119(self): + def test119(self) -> None: hn = HumanName("Lord God Almighty") self.m(hn.title, "Lord", hn) self.m(hn.first, "God", hn) @@ -1061,80 +1135,80 @@ def test119(self): class HumanNameConjunctionTestCase(HumanNameTestBase): # Last name with conjunction - def test_last_name_with_conjunction(self): + def test_last_name_with_conjunction(self) -> None: hn = HumanName('Jose Aznar y Lopez') self.m(hn.first, "Jose", hn) self.m(hn.last, "Aznar y Lopez", hn) - def test_multiple_conjunctions(self): + def test_multiple_conjunctions(self) -> None: hn = HumanName("part1 of The part2 of the part3 and part4") self.m(hn.first, "part1 of The part2 of the part3 and part4", hn) - def test_multiple_conjunctions2(self): + def test_multiple_conjunctions2(self) -> None: hn = HumanName("part1 of and The part2 of the part3 And part4") self.m(hn.first, "part1 of and The part2 of the part3 And part4", hn) - - def test_ends_with_conjunction(self): + + def test_ends_with_conjunction(self) -> None: hn = HumanName("Jon Dough and") self.m(hn.first, "Jon", hn) self.m(hn.last, "Dough and", hn) - def test_ends_with_two_conjunctions(self): + def test_ends_with_two_conjunctions(self) -> None: hn = HumanName("Jon Dough and of") self.m(hn.first, "Jon", hn) self.m(hn.last, "Dough and of", hn) - def test_starts_with_conjunction(self): + def test_starts_with_conjunction(self) -> None: hn = HumanName("and Jon Dough") self.m(hn.first, "and Jon", hn) self.m(hn.last, "Dough", hn) - def test_starts_with_two_conjunctions(self): + def test_starts_with_two_conjunctions(self) -> None: hn = HumanName("the and Jon Dough") self.m(hn.first, "the and Jon", hn) self.m(hn.last, "Dough", hn) # Potential conjunction/prefix treated as initial (because uppercase) - def test_uppercase_middle_initial_conflict_with_conjunction(self): + def test_uppercase_middle_initial_conflict_with_conjunction(self) -> None: hn = HumanName('John E Smith') self.m(hn.first, "John", hn) self.m(hn.middle, "E", hn) self.m(hn.last, "Smith", hn) - def test_lowercase_middle_initial_with_period_conflict_with_conjunction(self): + def test_lowercase_middle_initial_with_period_conflict_with_conjunction(self) -> None: hn = HumanName('john e. smith') self.m(hn.first, "john", hn) self.m(hn.middle, "e.", hn) self.m(hn.last, "smith", hn) # The conjunction "e" can also be an initial - def test_lowercase_first_initial_conflict_with_conjunction(self): + def test_lowercase_first_initial_conflict_with_conjunction(self) -> None: hn = HumanName('e j smith') self.m(hn.first, "e", hn) self.m(hn.middle, "j", hn) self.m(hn.last, "smith", hn) - def test_lowercase_middle_initial_conflict_with_conjunction(self): + def test_lowercase_middle_initial_conflict_with_conjunction(self) -> None: hn = HumanName('John e Smith') self.m(hn.first, "John", hn) self.m(hn.middle, "e", hn) self.m(hn.last, "Smith", hn) - def test_lowercase_middle_initial_and_suffix_conflict_with_conjunction(self): + def test_lowercase_middle_initial_and_suffix_conflict_with_conjunction(self) -> None: hn = HumanName('John e Smith, III') self.m(hn.first, "John", hn) self.m(hn.middle, "e", hn) self.m(hn.last, "Smith", hn) self.m(hn.suffix, "III", hn) - def test_lowercase_middle_initial_and_nocomma_suffix_conflict_with_conjunction(self): + def test_lowercase_middle_initial_and_nocomma_suffix_conflict_with_conjunction(self) -> None: hn = HumanName('John e Smith III') self.m(hn.first, "John", hn) self.m(hn.middle, "e", hn) self.m(hn.last, "Smith", hn) self.m(hn.suffix, "III", hn) - def test_lowercase_middle_initial_comma_lastname_and_suffix_conflict_with_conjunction(self): + def test_lowercase_middle_initial_comma_lastname_and_suffix_conflict_with_conjunction(self) -> None: hn = HumanName('Smith, John e, III, Jr') self.m(hn.first, "John", hn) self.m(hn.middle, "e", hn) @@ -1142,73 +1216,81 @@ def test_lowercase_middle_initial_comma_lastname_and_suffix_conflict_with_conjun self.m(hn.suffix, "III, Jr", hn) @unittest.expectedFailure - def test_two_initials_conflict_with_conjunction(self): + def test_two_initials_conflict_with_conjunction(self) -> None: # Supporting this seems to screw up titles with periods in them like M.B.A. hn = HumanName('E.T. Smith') self.m(hn.first, "E.", hn) self.m(hn.middle, "T.", hn) self.m(hn.last, "Smith", hn) - def test_couples_names(self): + def test_couples_names(self) -> None: hn = HumanName('John and Jane Smith') self.m(hn.first, "John and Jane", hn) self.m(hn.last, "Smith", hn) - def test_couples_names_with_conjunction_lastname(self): + def test_couples_names_with_conjunction_lastname(self) -> None: hn = HumanName('John and Jane Aznar y Lopez') self.m(hn.first, "John and Jane", hn) self.m(hn.last, "Aznar y Lopez", hn) - def test_couple_titles(self): + def test_couple_titles(self) -> None: hn = HumanName('Mr. and Mrs. John and Jane Smith') self.m(hn.title, "Mr. and Mrs.", hn) self.m(hn.first, "John and Jane", hn) self.m(hn.last, "Smith", hn) - def test_title_with_three_part_name_last_initial_is_suffix_uppercase_no_period(self): + def test_title_with_three_part_name_last_initial_is_suffix_uppercase_no_period(self) -> None: hn = HumanName("King John Alexander V") self.m(hn.title, "King", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Alexander", hn) self.m(hn.suffix, "V", hn) - def test_four_name_parts_with_suffix_that_could_be_initial_lowercase_no_period(self): + def test_four_name_parts_with_suffix_that_could_be_initial_lowercase_no_period(self) -> None: hn = HumanName("larry james edward johnson v") self.m(hn.first, "larry", hn) self.m(hn.middle, "james edward", hn) self.m(hn.last, "johnson", hn) self.m(hn.suffix, "v", hn) - def test_four_name_parts_with_suffix_that_could_be_initial_uppercase_no_period(self): + def test_four_name_parts_with_suffix_that_could_be_initial_uppercase_no_period(self) -> None: hn = HumanName("Larry James Johnson I") self.m(hn.first, "Larry", hn) self.m(hn.middle, "James", hn) self.m(hn.last, "Johnson", hn) self.m(hn.suffix, "I", hn) - def test_roman_numeral_initials(self): + def test_roman_numeral_initials(self) -> None: hn = HumanName("Larry V I") self.m(hn.first, "Larry", hn) self.m(hn.middle, "V", hn) self.m(hn.last, "I", hn) self.m(hn.suffix, "", hn) + def test_roman_numeral_suffix_not_in_suffix_list(self) -> None: + # VI-X are not in the suffix word lists, so they reach the + # is_roman_numeral(nxt) branch rather than are_suffixes() + hn = HumanName("John Smith VI") + self.m(hn.first, "John", hn) + self.m(hn.last, "Smith", hn) + self.m(hn.suffix, "VI", hn) + # tests for Rev. title (Reverend) - def test124(self): + def test124(self) -> None: hn = HumanName("Rev. John A. Kenneth Doe") self.m(hn.title, "Rev.", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test125(self): + def test125(self) -> None: hn = HumanName("Rev John A. Kenneth Doe") self.m(hn.title, "Rev", hn) self.m(hn.middle, "A. Kenneth", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) - def test126(self): + def test126(self) -> None: hn = HumanName("Doe, Rev. John A. Jr.") self.m(hn.title, "Rev.", hn) self.m(hn.first, "John", hn) @@ -1216,65 +1298,71 @@ def test126(self): self.m(hn.middle, "A.", hn) self.m(hn.suffix, "Jr.", hn) - def test127(self): + def test127(self) -> None: hn = HumanName("Buca di Beppo") self.m(hn.first, "Buca", hn) self.m(hn.last, "di Beppo", hn) - def test_le_as_last_name(self): + def test_le_as_last_name(self) -> None: hn = HumanName("Yin Le") self.m(hn.first, "Yin", hn) self.m(hn.last, "Le", hn) - def test_le_as_last_name_with_middle_initial(self): + def test_le_as_last_name_with_middle_initial(self) -> None: hn = HumanName("Yin a Le") self.m(hn.first, "Yin", hn) self.m(hn.middle, "a", hn) self.m(hn.last, "Le", hn) - - def test_conjunction_in_an_address_with_a_title(self): + + def test_conjunction_in_an_address_with_a_title(self) -> None: hn = HumanName("His Excellency Lord Duncan") self.m(hn.title, "His Excellency Lord", hn) self.m(hn.last, "Duncan", hn) - + @unittest.expectedFailure - def test_conjunction_in_an_address_with_a_first_name_title(self): + def test_conjunction_in_an_address_with_a_first_name_title(self) -> None: hn = HumanName("Her Majesty Queen Elizabeth") self.m(hn.title, "Her Majesty Queen", hn) # if you want to be technical, Queen is in FIRST_NAME_TITLES self.m(hn.first, "Elizabeth", hn) - def test_name_is_conjunctions(self): + def test_name_is_conjunctions(self) -> None: hn = HumanName("e and e") self.m(hn.first, "e and e", hn) class ConstantsCustomization(HumanNameTestBase): - def test_add_title(self): + def test_add_title(self) -> None: hn = HumanName("Te Awanui-a-Rangi Black", constants=None) + start_len = len(hn.C.titles) + self.assertTrue(start_len > 0) hn.C.titles.add('te') + self.assertEqual(start_len + 1, len(hn.C.titles)) hn.parse_full_name() - self.m(hn.title,"Te", hn) - self.m(hn.first,"Awanui-a-Rangi", hn) - self.m(hn.last,"Black", hn) - - def test_remove_title(self): + self.m(hn.title, "Te", hn) + self.m(hn.first, "Awanui-a-Rangi", hn) + self.m(hn.last, "Black", hn) + + def test_remove_title(self) -> None: hn = HumanName("Hon Solo", constants=None) + start_len = len(hn.C.titles) + self.assertTrue(start_len > 0) hn.C.titles.remove('hon') + self.assertEqual(start_len - 1, len(hn.C.titles)) hn.parse_full_name() - self.m(hn.first,"Hon", hn) - self.m(hn.last,"Solo", hn) - - def test_add_multiple_arguments(self): + self.m(hn.first, "Hon", hn) + self.m(hn.last, "Solo", hn) + + def test_add_multiple_arguments(self) -> None: hn = HumanName("Assoc Dean of Chemistry Robert Johns", constants=None) hn.C.titles.add('dean', 'Chemistry') hn.parse_full_name() - self.m(hn.title,"Assoc Dean of Chemistry", hn) - self.m(hn.first,"Robert", hn) - self.m(hn.last,"Johns", hn) + self.m(hn.title, "Assoc Dean of Chemistry", hn) + self.m(hn.first, "Robert", hn) + self.m(hn.last, "Johns", hn) - def test_instances_can_have_own_constants(self): + def test_instances_can_have_own_constants(self) -> None: hn = HumanName("", None) hn2 = HumanName("") hn.C.titles.remove('hon') @@ -1282,9 +1370,8 @@ def test_instances_can_have_own_constants(self): self.assertEqual(hn.has_own_config, True) self.assertEqual('hon' in hn2.C.titles, True) self.assertEqual(hn2.has_own_config, False) - - - def test_can_change_global_constants(self): + + def test_can_change_global_constants(self) -> None: hn = HumanName("") hn2 = HumanName("") hn.C.titles.remove('hon') @@ -1294,25 +1381,25 @@ def test_can_change_global_constants(self): self.assertEqual(hn2.has_own_config, False) # clean up so we don't mess up other tests hn.C.titles.add('hon') - - def test_remove_multiple_arguments(self): + + def test_remove_multiple_arguments(self) -> None: hn = HumanName("Ms Hon Solo", constants=None) hn.C.titles.remove('hon', 'ms') hn.parse_full_name() - self.m(hn.first,"Ms", hn) - self.m(hn.middle,"Hon", hn) - self.m(hn.last,"Solo", hn) + self.m(hn.first, "Ms", hn) + self.m(hn.middle, "Hon", hn) + self.m(hn.last, "Solo", hn) - def test_chain_multiple_arguments(self): + def test_chain_multiple_arguments(self) -> None: hn = HumanName("Dean Ms Hon Solo", constants=None) hn.C.titles.remove('hon', 'ms').add('dean') hn.parse_full_name() - self.m(hn.title,"Dean", hn) - self.m(hn.first,"Ms", hn) - self.m(hn.middle,"Hon", hn) - self.m(hn.last,"Solo", hn) + self.m(hn.title, "Dean", hn) + self.m(hn.first, "Ms", hn) + self.m(hn.middle, "Hon", hn) + self.m(hn.last, "Solo", hn) - def test_empty_attribute_default(self): + def test_empty_attribute_default(self) -> None: from nameparser.config import CONSTANTS _orig = CONSTANTS.empty_attribute_default CONSTANTS.empty_attribute_default = None @@ -1325,7 +1412,7 @@ def test_empty_attribute_default(self): self.m(hn.nickname, None, hn) CONSTANTS.empty_attribute_default = _orig - def test_empty_attribute_on_instance(self): + def test_empty_attribute_on_instance(self) -> None: hn = HumanName("", None) hn.C.empty_attribute_default = None self.m(hn.title, None, hn) @@ -1335,144 +1422,320 @@ def test_empty_attribute_on_instance(self): self.m(hn.suffix, None, hn) self.m(hn.nickname, None, hn) - def test_none_empty_attribute_string_formatting(self): + def test_none_empty_attribute_string_formatting(self) -> None: hn = HumanName("", None) hn.C.empty_attribute_default = None self.assertEqual('', str(hn), hn) - def test_add_constant_with_explicit_encoding(self): + def test_add_constant_with_explicit_encoding(self) -> None: c = Constants() c.titles.add_with_encoding(b'b\351ck', encoding='latin_1') self.assertIn('béck', c.titles) -class HumanNameNicknameTestCase(HumanNameTestBase): +class NicknameTestCase(HumanNameTestBase): # https://code.google.com/p/python-nameparser/issues/detail?id=33 - def test_nickname_in_parenthesis(self): + def test_nickname_in_parenthesis(self) -> None: hn = HumanName("Benjamin (Ben) Franklin") self.m(hn.first, "Benjamin", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Franklin", hn) self.m(hn.nickname, "Ben", hn) - - def test_nickname_in_parenthesis_with_comma(self): + + def test_two_word_nickname_in_parenthesis(self) -> None: + hn = HumanName("Benjamin (Big Ben) Franklin") + self.m(hn.first, "Benjamin", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "Franklin", hn) + self.m(hn.nickname, "Big Ben", hn) + + def test_two_words_in_quotes(self) -> None: + hn = HumanName('Benjamin "Big Ben" Franklin') + self.m(hn.first, "Benjamin", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "Franklin", hn) + self.m(hn.nickname, "Big Ben", hn) + + def test_nickname_in_parenthesis_with_comma(self) -> None: hn = HumanName("Franklin, Benjamin (Ben)") self.m(hn.first, "Benjamin", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Franklin", hn) self.m(hn.nickname, "Ben", hn) - - def test_nickname_in_parenthesis_with_comma_and_suffix(self): + + def test_nickname_in_parenthesis_with_comma_and_suffix(self) -> None: hn = HumanName("Franklin, Benjamin (Ben), Jr.") self.m(hn.first, "Benjamin", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Franklin", hn) self.m(hn.suffix, "Jr.", hn) self.m(hn.nickname, "Ben", hn) - - # it would be hard to support this without breaking some of the - # other examples with single quotes in the names. - @unittest.expectedFailure - def test_nickname_in_single_quotes(self): + + def test_nickname_in_single_quotes(self) -> None: hn = HumanName("Benjamin 'Ben' Franklin") self.m(hn.first, "Benjamin", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Franklin", hn) self.m(hn.nickname, "Ben", hn) - def test_nickname_in_double_quotes(self): + def test_nickname_in_double_quotes(self) -> None: hn = HumanName("Benjamin \"Ben\" Franklin") self.m(hn.first, "Benjamin", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Franklin", hn) self.m(hn.nickname, "Ben", hn) - - def test_single_quotes_on_first_name_not_treated_as_nickname(self): - hn = HumanName("Brian O'connor") + + def test_single_quotes_on_first_name_not_treated_as_nickname(self) -> None: + hn = HumanName("Brian Andrew O'connor") self.m(hn.first, "Brian", hn) - self.m(hn.middle, "", hn) + self.m(hn.middle, "Andrew", hn) self.m(hn.last, "O'connor", hn) self.m(hn.nickname, "", hn) - - def test_single_quotes_on_both_name_not_treated_as_nickname(self): + + def test_single_quotes_on_both_name_not_treated_as_nickname(self) -> None: hn = HumanName("La'tanya O'connor") self.m(hn.first, "La'tanya", hn) self.m(hn.middle, "", hn) self.m(hn.last, "O'connor", hn) self.m(hn.nickname, "", hn) - - def test_single_quotes_on_end_of_last_name_not_treated_as_nickname(self): + + def test_single_quotes_on_end_of_last_name_not_treated_as_nickname(self) -> None: hn = HumanName("Mari' Aube'") self.m(hn.first, "Mari'", hn) self.m(hn.middle, "", hn) self.m(hn.last, "Aube'", hn) self.m(hn.nickname, "", hn) - - #http://code.google.com/p/python-nameparser/issues/detail?id=17 - def test_parenthesis_are_removed(self): - hn = HumanName("John Jones (Google Docs)") + + def test_okina_inside_name_not_treated_as_nickname(self) -> None: + hn = HumanName("Harrieta Keōpūolani Nāhiʻenaʻena") + self.m(hn.first, "Harrieta", hn) + self.m(hn.middle, "Keōpūolani", hn) + self.m(hn.last, "Nāhiʻenaʻena", hn) + self.m(hn.nickname, "", hn) + + def test_single_quotes_not_treated_as_nickname_Hawaiian_example(self) -> None: + hn = HumanName("Harietta Keopuolani Nahi'ena'ena") + self.m(hn.first, "Harietta", hn) + self.m(hn.middle, "Keopuolani", hn) + self.m(hn.last, "Nahi'ena'ena", hn) + self.m(hn.nickname, "", hn) + + def test_single_quotes_not_treated_as_nickname_Kenyan_example(self) -> None: + hn = HumanName("Naomi Wambui Ng'ang'a") + self.m(hn.first, "Naomi", hn) + self.m(hn.middle, "Wambui", hn) + self.m(hn.last, "Ng'ang'a", hn) + self.m(hn.nickname, "", hn) + + def test_single_quotes_not_treated_as_nickname_Samoan_example(self) -> None: + hn = HumanName("Va'apu'u Vitale") + self.m(hn.first, "Va'apu'u", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "Vitale", hn) + self.m(hn.nickname, "", hn) + + # http://code.google.com/p/python-nameparser/issues/detail?id=17 + def test_parenthesis_are_removed_from_name(self) -> None: + hn = HumanName("John Jones (Unknown)") self.m(hn.first, "John", hn) self.m(hn.last, "Jones", hn) # not testing the nicknames because we don't actually care - # about Google Docs. - - def test_parenthesis_are_removed2(self): + # about Google Docs here + + def test_duplicate_parenthesis_are_removed_from_name(self) -> None: hn = HumanName("John Jones (Google Docs), Jr. (Unknown)") self.m(hn.first, "John", hn) self.m(hn.last, "Jones", hn) self.m(hn.suffix, "Jr.", hn) + def test_nickname_and_last_name(self) -> None: + hn = HumanName('"Rick" Edmonds') + self.m(hn.first, "", hn) + self.m(hn.last, "Edmonds", hn) + self.m(hn.nickname, "Rick", hn) + + @unittest.expectedFailure + def test_nickname_and_last_name_with_title(self) -> None: + hn = HumanName('Senator "Rick" Edmonds') + self.m(hn.title, "Senator", hn) + self.m(hn.first, "", hn) + self.m(hn.last, "Edmonds", hn) + self.m(hn.nickname, "Rick", hn) + + +# class MaidenNameTestCase(HumanNameTestBase): +# +# def test_parenthesis_and_quotes_together(self): +# hn = HumanName("Jennifer 'Jen' Jones (Duff)") +# self.m(hn.first, "Jennifer", hn) +# self.m(hn.last, "Jones", hn) +# self.m(hn.nickname, "Jen", hn) +# self.m(hn.maiden, "Duff", hn) +# +# def test_maiden_name_with_nee(self): +# # https://en.wiktionary.org/wiki/née +# hn = HumanName("Mary Toogood nee Johnson") +# self.m(hn.first, "Mary", hn) +# self.m(hn.last, "Toogood", hn) +# self.m(hn.maiden, "Johnson", hn) +# +# def test_maiden_name_with_accented_nee(self): +# # https://en.wiktionary.org/wiki/née +# hn = HumanName("Mary Toogood née Johnson") +# self.m(hn.first, "Mary", hn) +# self.m(hn.last, "Toogood", hn) +# self.m(hn.maiden, "Johnson", hn) +# +# def test_maiden_name_with_nee_and_comma(self): +# # https://en.wiktionary.org/wiki/née +# hn = HumanName("Mary Toogood, née Johnson") +# self.m(hn.first, "Mary", hn) +# self.m(hn.last, "Toogood", hn) +# self.m(hn.maiden, "Johnson", hn) +# +# def test_maiden_name_with_nee_with_parenthesis(self): +# hn = HumanName("Mary Toogood (nee Johnson)") +# self.m(hn.first, "Mary", hn) +# self.m(hn.last, "Toogood", hn) +# self.m(hn.maiden, "Johnson", hn) +# +# def test_maiden_name_with_parenthesis(self): +# hn = HumanName("Mary Toogood (Johnson)") +# self.m(hn.first, "Mary", hn) +# self.m(hn.last, "Toogood", hn) +# self.m(hn.maiden, "Johnson", hn) +# + class PrefixesTestCase(HumanNameTestBase): - def test_prefix(self): + def test_prefix(self) -> None: hn = HumanName("Juan del Sur") self.m(hn.first, "Juan", hn) self.m(hn.last, "del Sur", hn) - - def test_prefix_with_period(self): + + def test_prefix_with_period(self) -> None: hn = HumanName("Jill St. John") self.m(hn.first, "Jill", hn) self.m(hn.last, "St. John", hn) - - def test_prefix_before_two_part_last_name(self): + + def test_prefix_before_two_part_last_name(self) -> None: hn = HumanName("pennie von bergen wessels") self.m(hn.first, "pennie", hn) self.m(hn.last, "von bergen wessels", hn) - def test_prefix_before_two_part_last_name_with_suffix(self): + def test_prefix_is_first_name(self) -> None: + hn = HumanName("Van Johnson") + self.m(hn.first, "Van", hn) + self.m(hn.last, "Johnson", hn) + + def test_prefix_is_first_name_with_middle_name(self) -> None: + hn = HumanName("Van Jeremy Johnson") + self.m(hn.first, "Van", hn) + self.m(hn.middle, "Jeremy", hn) + self.m(hn.last, "Johnson", hn) + + def test_prefix_before_two_part_last_name_with_suffix(self) -> None: hn = HumanName("pennie von bergen wessels III") self.m(hn.first, "pennie", hn) self.m(hn.last, "von bergen wessels", hn) self.m(hn.suffix, "III", hn) - def test_two_part_last_name_with_suffix_comma(self): + def test_prefix_before_two_part_last_name_with_acronym_suffix(self) -> None: + hn = HumanName("pennie von bergen wessels M.D.") + self.m(hn.first, "pennie", hn) + self.m(hn.last, "von bergen wessels", hn) + self.m(hn.suffix, "M.D.", hn) + + def test_two_part_last_name_with_suffix_comma(self) -> None: hn = HumanName("pennie von bergen wessels, III") self.m(hn.first, "pennie", hn) self.m(hn.last, "von bergen wessels", hn) self.m(hn.suffix, "III", hn) - def test_two_part_last_name_with_suffix(self): + def test_two_part_last_name_with_suffix(self) -> None: hn = HumanName("von bergen wessels, pennie III") self.m(hn.first, "pennie", hn) self.m(hn.last, "von bergen wessels", hn) self.m(hn.suffix, "III", hn) + def test_last_name_two_part_last_name_with_two_suffixes(self) -> None: + hn = HumanName("von bergen wessels MD, pennie III") + self.m(hn.first, "pennie", hn) + self.m(hn.last, "von bergen wessels", hn) + self.m(hn.suffix, "MD, III", hn) + + def test_comma_two_part_last_name_with_acronym_suffix(self) -> None: + hn = HumanName("von bergen wessels, pennie MD") + self.m(hn.first, "pennie", hn) + self.m(hn.last, "von bergen wessels", hn) + self.m(hn.suffix, "MD", hn) + + def test_comma_two_part_last_name_with_suffix_in_first_part(self) -> None: + # I'm kinda surprised this works, not really sure if this is a + # realistic place for a suffix to be. + hn = HumanName("von bergen wessels MD, pennie") + self.m(hn.first, "pennie", hn) + self.m(hn.last, "von bergen wessels", hn) + self.m(hn.suffix, "MD", hn) + + def test_title_two_part_last_name_with_suffix_in_first_part(self) -> None: + hn = HumanName("pennie von bergen wessels MD, III") + self.m(hn.first, "pennie", hn) + self.m(hn.last, "von bergen wessels", hn) + self.m(hn.suffix, "MD, III", hn) + + def test_portuguese_dos(self) -> None: + hn = HumanName("Rafael Sousa dos Anjos") + self.m(hn.first, "Rafael", hn) + self.m(hn.middle, "Sousa", hn) + self.m(hn.last, "dos Anjos", hn) + + def test_portuguese_prefixes(self) -> None: + hn = HumanName("Joao da Silva do Amaral de Souza") + self.m(hn.first, "Joao", hn) + self.m(hn.middle, "da Silva do Amaral", hn) + self.m(hn.last, "de Souza", hn) + + def test_three_conjunctions(self) -> None: + hn = HumanName("Dr. Juan Q. Xavier de la dos Vega III") + self.m(hn.first, "Juan", hn) + self.m(hn.last, "de la dos Vega", hn) + self.m(hn.title, "Dr.", hn) + self.m(hn.middle, "Q. Xavier", hn) + self.m(hn.suffix, "III", hn) + + def test_lastname_three_conjunctions(self) -> None: + hn = HumanName("de la dos Vega, Dr. Juan Q. Xavier III") + self.m(hn.first, "Juan", hn) + self.m(hn.last, "de la dos Vega", hn) + self.m(hn.title, "Dr.", hn) + self.m(hn.middle, "Q. Xavier", hn) + self.m(hn.suffix, "III", hn) + + def test_comma_three_conjunctions(self) -> None: + hn = HumanName("Dr. Juan Q. Xavier de la dos Vega, III") + self.m(hn.first, "Juan", hn) + self.m(hn.last, "de la dos Vega", hn) + self.m(hn.title, "Dr.", hn) + self.m(hn.middle, "Q. Xavier", hn) + self.m(hn.suffix, "III", hn) + class SuffixesTestCase(HumanNameTestBase): - - def test_suffix(self): + + def test_suffix(self) -> None: hn = HumanName("Joe Franklin Jr") self.m(hn.first, "Joe", hn) self.m(hn.last, "Franklin", hn) self.m(hn.suffix, "Jr", hn) - def test_suffix_with_periods(self): + def test_suffix_with_periods(self) -> None: hn = HumanName("Joe Dentist D.D.S.") self.m(hn.first, "Joe", hn) self.m(hn.last, "Dentist", hn) self.m(hn.suffix, "D.D.S.", hn) - def test_two_suffixes(self): + def test_two_suffixes(self) -> None: hn = HumanName("Kenneth Clarke QC MP") self.m(hn.first, "Kenneth", hn) self.m(hn.last, "Clarke", hn) @@ -1480,86 +1743,85 @@ def test_two_suffixes(self): # not ideal but at least its in the right bucket self.m(hn.suffix, "QC, MP", hn) - def test_two_suffixes_lastname_comma_format(self): + def test_two_suffixes_lastname_comma_format(self) -> None: hn = HumanName("Washington Jr. MD, Franklin") self.m(hn.first, "Franklin", hn) self.m(hn.last, "Washington", hn) # NOTE: this adds a comma when the original format did not have one. self.m(hn.suffix, "Jr., MD", hn) - def test_two_suffixes_suffix_comma_format(self): + def test_two_suffixes_suffix_comma_format(self) -> None: hn = HumanName("Franklin Washington, Jr. MD") self.m(hn.first, "Franklin", hn) self.m(hn.last, "Washington", hn) self.m(hn.suffix, "Jr. MD", hn) - def test_suffix_containing_periods(self): + def test_suffix_containing_periods(self) -> None: hn = HumanName("Kenneth Clarke Q.C.") self.m(hn.first, "Kenneth", hn) self.m(hn.last, "Clarke", hn) self.m(hn.suffix, "Q.C.", hn) - def test_suffix_containing_periods_lastname_comma_format(self): + def test_suffix_containing_periods_lastname_comma_format(self) -> None: hn = HumanName("Clarke, Kenneth, Q.C. M.P.") self.m(hn.first, "Kenneth", hn) self.m(hn.last, "Clarke", hn) self.m(hn.suffix, "Q.C. M.P.", hn) - def test_suffix_containing_periods_suffix_comma_format(self): + def test_suffix_containing_periods_suffix_comma_format(self) -> None: hn = HumanName("Kenneth Clarke Q.C., M.P.") self.m(hn.first, "Kenneth", hn) self.m(hn.last, "Clarke", hn) self.m(hn.suffix, "Q.C., M.P.", hn) - def test_suffix_with_single_comma_format(self): + def test_suffix_with_single_comma_format(self) -> None: hn = HumanName("John Doe jr., MD") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "jr., MD", hn) - def test_suffix_with_double_comma_format(self): + def test_suffix_with_double_comma_format(self) -> None: hn = HumanName("Doe, John jr., MD") self.m(hn.first, "John", hn) self.m(hn.last, "Doe", hn) self.m(hn.suffix, "jr., MD", hn) - @unittest.expectedFailure - def test_phd_with_erroneous_space(self): + def test_phd_with_erroneous_space(self) -> None: hn = HumanName("John Smith, Ph. D.") self.m(hn.first, "John", hn) self.m(hn.last, "Smith", hn) self.m(hn.suffix, "Ph. D.", hn) - #http://en.wikipedia.org/wiki/Ma_(surname) - def test_potential_suffix_that_is_also_last_name(self): + def test_phd_extracted_without_comma(self) -> None: + hn = HumanName("John Smith Ph. D.") + self.m(hn.first, "John", hn) + self.m(hn.last, "Smith", hn) + self.m(hn.suffix, "Ph. D.", hn) + + def test_phd_conflict(self) -> None: + hn = HumanName("Adolph D") + self.m(hn.first, "Adolph", hn) + self.m(hn.last, "D", hn) + + # http://en.wikipedia.org/wiki/Ma_(surname) + + def test_potential_suffix_that_is_also_last_name(self) -> None: hn = HumanName("Jack Ma") self.m(hn.first, "Jack", hn) self.m(hn.last, "Ma", hn) - - def test_potential_suffix_that_is_also_last_name_comma(self): + + def test_potential_suffix_that_is_also_last_name_comma(self) -> None: hn = HumanName("Ma, Jack") self.m(hn.first, "Jack", hn) self.m(hn.last, "Ma", hn) - - def test_potential_suffix_that_is_also_first_name_comma(self): - hn = HumanName("Johnson, Bart") - self.m(hn.first, "Bart", hn) - self.m(hn.last, "Johnson", hn) - - # TODO: handle conjunctions in last names followed by first names clashing with suffixes - @unittest.expectedFailure - def test_potential_suffix_that_is_also_first_name_comma_with_conjunction(self): - hn = HumanName("De la Vina, Bart") - self.m(hn.first, "Bart", hn) - self.m(hn.last, "De la Vina", hn) - - def test_potential_suffix_that_is_also_last_name_with_suffix(self): + + def test_potential_suffix_that_is_also_last_name_with_suffix(self) -> None: hn = HumanName("Jack Ma Jr") self.m(hn.first, "Jack", hn) self.m(hn.last, "Ma", hn) self.m(hn.suffix, "Jr", hn) - def test_potential_suffix_that_is_also_last_name_with_suffix_comma(self): + def test_potential_suffix_that_is_also_last_name_with_suffix_comma(self) -> None: hn = HumanName("Ma III, Jack Jr") self.m(hn.first, "Jack", hn) self.m(hn.last, "Ma", hn) @@ -1567,40 +1829,40 @@ def test_potential_suffix_that_is_also_last_name_with_suffix_comma(self): # https://github.com/derek73/python-nameparser/issues/27 @unittest.expectedFailure - def test_king(self): + def test_king(self) -> None: hn = HumanName("Dr King Jr") self.m(hn.title, "Dr", hn) self.m(hn.last, "King", hn) self.m(hn.suffix, "Jr", hn) - def test_suffix_with_periods(self): + def test_multiple_letter_suffix_with_periods(self) -> None: hn = HumanName("John Doe Msc.Ed.") - self.m(hn.first,"John", hn) - self.m(hn.last,"Doe", hn) - self.m(hn.suffix,"Msc.Ed.", hn) + self.m(hn.first, "John", hn) + self.m(hn.last, "Doe", hn) + self.m(hn.suffix, "Msc.Ed.", hn) - def test_suffix_with_periods_with_comma(self): + def test_suffix_with_periods_with_comma(self) -> None: hn = HumanName("John Doe, Msc.Ed.") - self.m(hn.first,"John", hn) - self.m(hn.last,"Doe", hn) - self.m(hn.suffix,"Msc.Ed.", hn) + self.m(hn.first, "John", hn) + self.m(hn.last, "Doe", hn) + self.m(hn.suffix, "Msc.Ed.", hn) - def test_suffix_with_periods_with_lastname_comma(self): + def test_suffix_with_periods_with_lastname_comma(self) -> None: hn = HumanName("Doe, John Msc.Ed.") - self.m(hn.first,"John", hn) - self.m(hn.last,"Doe", hn) - self.m(hn.suffix,"Msc.Ed.", hn) + self.m(hn.first, "John", hn) + self.m(hn.last, "Doe", hn) + self.m(hn.suffix, "Msc.Ed.", hn) class TitleTestCase(HumanNameTestBase): - def test_last_name_is_also_title(self): + def test_last_name_is_also_title(self) -> None: hn = HumanName("Amy E Maid") self.m(hn.first, "Amy", hn) self.m(hn.middle, "E", hn) self.m(hn.last, "Maid", hn) - def test_last_name_is_also_title_no_comma(self): + def test_last_name_is_also_title_no_comma(self) -> None: hn = HumanName("Dr. Martin Luther King Jr.") self.m(hn.title, "Dr.", hn) self.m(hn.first, "Martin", hn) @@ -1608,65 +1870,65 @@ def test_last_name_is_also_title_no_comma(self): self.m(hn.last, "King", hn) self.m(hn.suffix, "Jr.", hn) - def test_last_name_is_also_title_with_comma(self): - hn = HumanName("Duke Martin Luther King, Jr.") - self.m(hn.title, "Duke", hn) + def test_last_name_is_also_title_with_comma(self) -> None: + hn = HumanName("Dr Martin Luther King, Jr.") + self.m(hn.title, "Dr", hn) self.m(hn.first, "Martin", hn) self.m(hn.middle, "Luther", hn) self.m(hn.last, "King", hn) self.m(hn.suffix, "Jr.", hn) - def test_last_name_is_also_title3(self): + def test_last_name_is_also_title3(self) -> None: hn = HumanName("John King") self.m(hn.first, "John", hn) self.m(hn.last, "King", hn) - def test_title_with_conjunction(self): + def test_title_with_conjunction(self) -> None: hn = HumanName("Secretary of State Hillary Clinton") self.m(hn.title, "Secretary of State", hn) self.m(hn.first, "Hillary", hn) self.m(hn.last, "Clinton", hn) - def test_compound_title_with_conjunction(self): + def test_compound_title_with_conjunction(self) -> None: hn = HumanName("Cardinal Secretary of State Hillary Clinton") self.m(hn.title, "Cardinal Secretary of State", hn) self.m(hn.first, "Hillary", hn) self.m(hn.last, "Clinton", hn) - def test_title_is_title(self): + def test_title_is_title(self) -> None: hn = HumanName("Coach") self.m(hn.title, "Coach", hn) # TODO: fix handling of U.S. @unittest.expectedFailure - def test_chained_title_first_name_initial(self): + def test_chained_title_first_name_title_is_initials(self) -> None: hn = HumanName("U.S. District Judge Marc Thomas Treadwell") self.m(hn.title, "U.S. District Judge", hn) self.m(hn.first, "Marc", hn) self.m(hn.middle, "Thomas", hn) self.m(hn.last, "Treadwell", hn) - - def test_conflict_with_chained_title_first_name_initial(self): + + def test_conflict_with_chained_title_first_name_initial(self) -> None: hn = HumanName("U. S. Grant") self.m(hn.first, "U.", hn) self.m(hn.middle, "S.", hn) self.m(hn.last, "Grant", hn) - - def test_chained_title_first_name_initial(self): + + def test_chained_title_first_name_initial_with_no_period(self) -> None: hn = HumanName("US Magistrate Judge T Michael Putnam") self.m(hn.title, "US Magistrate Judge", hn) self.m(hn.first, "T", hn) self.m(hn.middle, "Michael", hn) self.m(hn.last, "Putnam", hn) - - def test_chained_hyphenated_title(self): + + def test_chained_hyphenated_title(self) -> None: hn = HumanName("US Magistrate-Judge Elizabeth E Campbell") self.m(hn.title, "US Magistrate-Judge", hn) self.m(hn.first, "Elizabeth", hn) self.m(hn.middle, "E", hn) self.m(hn.last, "Campbell", hn) - - def test_chained_hyphenated_title_with_comma_suffix(self): + + def test_chained_hyphenated_title_with_comma_suffix(self) -> None: hn = HumanName("Mag-Judge Harwell G Davis, III") self.m(hn.title, "Mag-Judge", hn) self.m(hn.first, "Harwell", hn) @@ -1675,47 +1937,47 @@ def test_chained_hyphenated_title_with_comma_suffix(self): self.m(hn.suffix, "III", hn) @unittest.expectedFailure - def test_title_multiple_titles_with_apostrophe_s(self): + def test_title_multiple_titles_with_apostrophe_s(self) -> None: hn = HumanName("The Right Hon. the President of the Queen's Bench Division") self.m(hn.title, "The Right Hon. the President of the Queen's Bench Division", hn) - def test_title_starts_with_conjunction(self): + def test_title_starts_with_conjunction(self) -> None: hn = HumanName("The Rt Hon John Jones") self.m(hn.title, "The Rt Hon", hn) self.m(hn.first, "John", hn) self.m(hn.last, "Jones", hn) - def test_conjunction_before_title(self): + def test_conjunction_before_title(self) -> None: hn = HumanName('The Lord of the Universe') self.m(hn.title, "The Lord of the Universe", hn) - def test_double_conjunction_on_title(self): + def test_double_conjunction_on_title(self) -> None: hn = HumanName('Lord of the Universe') self.m(hn.title, "Lord of the Universe", hn) - def test_triple_conjunction_on_title(self): + def test_triple_conjunction_on_title(self) -> None: hn = HumanName('Lord and of the Universe') self.m(hn.title, "Lord and of the Universe", hn) - def test_multiple_conjunctions_on_multiple_titles(self): + def test_multiple_conjunctions_on_multiple_titles(self) -> None: hn = HumanName('Lord of the Universe and Associate Supreme Queen of the World Lisa Simpson') self.m(hn.title, "Lord of the Universe and Associate Supreme Queen of the World", hn) self.m(hn.first, "Lisa", hn) self.m(hn.last, "Simpson", hn) - def test_title_with_last_initial_is_suffix(self): + def test_title_with_last_initial_is_suffix(self) -> None: hn = HumanName("King John V.") self.m(hn.title, "King", hn) self.m(hn.first, "John", hn) self.m(hn.last, "V.", hn) - - def test_initials_also_suffix(self): + + def test_initials_also_suffix(self) -> None: hn = HumanName("Smith, J.R.") self.m(hn.first, "J.R.", hn) # self.m(hn.middle, "R.", hn) self.m(hn.last, "Smith", hn) - def test_two_title_parts_separated_by_periods(self): + def test_two_title_parts_separated_by_periods(self) -> None: hn = HumanName("Lt.Gen. John A. Kenneth Doe IV") self.m(hn.title, "Lt.Gen.", hn) self.m(hn.first, "John", hn) @@ -1723,7 +1985,7 @@ def test_two_title_parts_separated_by_periods(self): self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "IV", hn) - def test_two_part_title(self): + def test_two_part_title(self) -> None: hn = HumanName("Lt. Gen. John A. Kenneth Doe IV") self.m(hn.title, "Lt. Gen.", hn) self.m(hn.first, "John", hn) @@ -1731,7 +1993,7 @@ def test_two_part_title(self): self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "IV", hn) - def test_two_part_title_with_lastname_comma(self): + def test_two_part_title_with_lastname_comma(self) -> None: hn = HumanName("Doe, Lt. Gen. John A. Kenneth IV") self.m(hn.title, "Lt. Gen.", hn) self.m(hn.first, "John", hn) @@ -1739,7 +2001,7 @@ def test_two_part_title_with_lastname_comma(self): self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "IV", hn) - def test_two_part_title_with_suffix_comma(self): + def test_two_part_title_with_suffix_comma(self) -> None: hn = HumanName("Lt. Gen. John A. Kenneth Doe, Jr.") self.m(hn.title, "Lt. Gen.", hn) self.m(hn.first, "John", hn) @@ -1747,7 +2009,7 @@ def test_two_part_title_with_suffix_comma(self): self.m(hn.middle, "A. Kenneth", hn) self.m(hn.suffix, "Jr.", hn) - def test_possible_conflict_with_middle_initial_that_could_be_suffix(self): + def test_possible_conflict_with_middle_initial_that_could_be_suffix(self) -> None: hn = HumanName("Doe, Rev. John V, Jr.") self.m(hn.title, "Rev.", hn) self.m(hn.first, "John", hn) @@ -1755,7 +2017,7 @@ def test_possible_conflict_with_middle_initial_that_could_be_suffix(self): self.m(hn.middle, "V", hn) self.m(hn.suffix, "Jr.", hn) - def test_possible_conflict_with_suffix_that_could_be_initial(self): + def test_possible_conflict_with_suffix_that_could_be_initial(self) -> None: hn = HumanName("Doe, Rev. John A., V, Jr.") self.m(hn.title, "Rev.", hn) self.m(hn.first, "John", hn) @@ -1766,48 +2028,69 @@ def test_possible_conflict_with_suffix_that_could_be_initial(self): # 'ben' is removed from PREFIXES in v0.2.5 # this test could re-enable this test if we decide to support 'ben' as a prefix @unittest.expectedFailure - def test_ben_as_conjunction(self): + def test_ben_as_conjunction(self) -> None: hn = HumanName("Ahmad ben Husain") - self.m(hn.first,"Ahmad", hn) - self.m(hn.last,"ben Husain", hn) + self.m(hn.first, "Ahmad", hn) + self.m(hn.last, "ben Husain", hn) - def test_ben_as_first_name(self): + def test_ben_as_first_name(self) -> None: hn = HumanName("Ben Johnson") self.m(hn.first, "Ben", hn) self.m(hn.last, "Johnson", hn) - def test_ben_as_first_name_with_middle_name(self): + def test_ben_as_first_name_with_middle_name(self) -> None: hn = HumanName("Ben Alex Johnson") self.m(hn.first, "Ben", hn) self.m(hn.middle, "Alex", hn) self.m(hn.last, "Johnson", hn) - def test_ben_as_middle_name(self): + def test_ben_as_middle_name(self) -> None: hn = HumanName("Alex Ben Johnson") self.m(hn.first, "Alex", hn) self.m(hn.middle, "Ben", hn) self.m(hn.last, "Johnson", hn) # http://code.google.com/p/python-nameparser/issues/detail?id=13 - def test_last_name_also_prefix(self): + def test_last_name_also_prefix(self) -> None: hn = HumanName("Jane Doctor") self.m(hn.first, "Jane", hn) self.m(hn.last, "Doctor", hn) - def test_title_with_periods(self): + def test_title_with_periods(self) -> None: hn = HumanName("Lt.Gov. John Doe") - self.m(hn.title,"Lt.Gov.", hn) - self.m(hn.first,"John", hn) - self.m(hn.last,"Doe", hn) + self.m(hn.title, "Lt.Gov.", hn) + self.m(hn.first, "John", hn) + self.m(hn.last, "Doe", hn) - def test_title_with_periods_lastname_comma(self): + def test_title_with_periods_lastname_comma(self) -> None: hn = HumanName("Doe, Lt.Gov. John") - self.m(hn.title,"Lt.Gov.", hn) - self.m(hn.first,"John", hn) - self.m(hn.last,"Doe", hn) + self.m(hn.title, "Lt.Gov.", hn) + self.m(hn.first, "John", hn) + self.m(hn.last, "Doe", hn) + + def test_mac_with_spaces(self) -> None: + hn = HumanName("Jane Mac Beth") + self.m(hn.first, "Jane", hn) + self.m(hn.last, "Mac Beth", hn) + + def test_mac_as_first_name(self) -> None: + hn = HumanName("Mac Miller") + self.m(hn.first, "Mac", hn) + self.m(hn.last, "Miller", hn) + + def test_multiple_prefixes(self) -> None: + hn = HumanName("Mike van der Velt") + self.m(hn.first, "Mike", hn) + self.m(hn.last, "van der Velt", hn) + + def test_2_same_prefixes_in_the_name(self) -> None: + hh = HumanName("Vincent van Gogh van Beethoven") + self.m(hh.first, "Vincent", hh) + self.m(hh.middle, "van Gogh", hh) + self.m(hh.last, "van Beethoven", hh) class HumanNameCapitalizationTestCase(HumanNameTestBase): - def test_capitalization_exception_for_III(self): + def test_capitalization_exception_for_III(self) -> None: hn = HumanName('juan q. xavier velasquez y garcia iii') hn.capitalize() self.m(str(hn), 'Juan Q. Xavier Velasquez y Garcia III', hn) @@ -1815,165 +2098,307 @@ def test_capitalization_exception_for_III(self): # FIXME: this test does not pass due to a known issue # http://code.google.com/p/python-nameparser/issues/detail?id=22 @unittest.expectedFailure - def test_capitalization_exception_for_already_capitalized_III_KNOWN_FAILURE(self): + def test_capitalization_exception_for_already_capitalized_III_KNOWN_FAILURE(self) -> None: hn = HumanName('juan garcia III') hn.capitalize() self.m(str(hn), 'Juan Garcia III', hn) - def test_capitalize_title(self): + def test_capitalize_title(self) -> None: hn = HumanName('lt. gen. john a. kenneth doe iv') hn.capitalize() self.m(str(hn), 'Lt. Gen. John A. Kenneth Doe IV', hn) - def test_capitalize_title_to_lower(self): + def test_capitalize_title_to_lower(self) -> None: hn = HumanName('LT. GEN. JOHN A. KENNETH DOE IV') hn.capitalize() self.m(str(hn), 'Lt. Gen. John A. Kenneth Doe IV', hn) # Capitalization with M(a)c and hyphenated names - def test_capitalization_with_Mac_as_hyphenated_names(self): + def test_capitalization_with_Mac_as_hyphenated_names(self) -> None: hn = HumanName('donovan mcnabb-smith') hn.capitalize() self.m(str(hn), 'Donovan McNabb-Smith', hn) - def test_capitization_middle_initial_is_also_a_conjunction(self): + def test_capitization_middle_initial_is_also_a_conjunction(self) -> None: hn = HumanName('scott e. werner') hn.capitalize() self.m(str(hn), 'Scott E. Werner', hn) # Leaving already-capitalized names alone - def test_no_change_to_mixed_chase(self): + def test_no_change_to_mixed_chase(self) -> None: hn = HumanName('Shirley Maclaine') hn.capitalize() self.m(str(hn), 'Shirley Maclaine', hn) - def test_force_capitalization(self): + def test_force_capitalization(self) -> None: hn = HumanName('Shirley Maclaine') hn.capitalize(force=True) self.m(str(hn), 'Shirley MacLaine', hn) - def test_capitalize_diacritics(self): + def test_capitalize_diacritics(self) -> None: hn = HumanName('matthëus schmidt') hn.capitalize() - self.m(u(hn), 'Matthëus Schmidt', hn) + self.m(str(hn), 'Matthëus Schmidt', hn) # http://code.google.com/p/python-nameparser/issues/detail?id=15 - def test_downcasing_mac(self): + def test_downcasing_mac(self) -> None: hn = HumanName('RONALD MACDONALD') hn.capitalize() self.m(str(hn), 'Ronald MacDonald', hn) # http://code.google.com/p/python-nameparser/issues/detail?id=23 - def test_downcasing_mc(self): + def test_downcasing_mc(self) -> None: hn = HumanName('RONALD MCDONALD') hn.capitalize() self.m(str(hn), 'Ronald McDonald', hn) - def test_short_names_with_mac(self): + def test_short_names_with_mac(self) -> None: hn = HumanName('mack johnson') hn.capitalize() self.m(str(hn), 'Mack Johnson', hn) + def test_portuguese_prefixes(self) -> None: + hn = HumanName("joao da silva do amaral de souza") + hn.capitalize() + self.m(str(hn), 'Joao da Silva do Amaral de Souza', hn) + + def test_capitalize_prefix_clash_on_first_name(self) -> None: + hn = HumanName("van nguyen") + hn.capitalize() + self.m(str(hn), 'Van Nguyen', hn) + + class HumanNameOutputFormatTests(HumanNameTestBase): - - def test_formatting_init_argument(self): - hn = HumanName("Rev John A. Kenneth Doe III (Kenny)", - string_format = "TEST1") - self.assertEqual(u(hn), "TEST1") - def test_formatting_constants_attribute(self): + def test_formatting_init_argument(self) -> None: + hn = HumanName("Rev John A. Kenneth Doe III (Kenny)", + string_format="TEST1") + self.assertEqual(str(hn), "TEST1") + + def test_formatting_constants_attribute(self) -> None: from nameparser.config import CONSTANTS _orig = CONSTANTS.string_format CONSTANTS.string_format = "TEST2" hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") - self.assertEqual(u(hn), "TEST2") + self.assertEqual(str(hn), "TEST2") CONSTANTS.string_format = _orig - def test_quote_nickname_formating(self): + def test_capitalize_name_constants_attribute(self) -> None: + from nameparser.config import CONSTANTS + CONSTANTS.capitalize_name = True + hn = HumanName("bob v. de la macdole-eisenhower phd") + self.assertEqual(str(hn), "Bob V. de la MacDole-Eisenhower Ph.D.") + CONSTANTS.capitalize_name = False + + def test_force_mixed_case_capitalization_constants_attribute(self) -> None: + from nameparser.config import CONSTANTS + CONSTANTS.force_mixed_case_capitalization = True + hn = HumanName('Shirley Maclaine') + hn.capitalize() + self.assertEqual(str(hn), "Shirley MacLaine") + CONSTANTS.force_mixed_case_capitalization = False + + def test_capitalize_name_and_force_mixed_case_capitalization_constants_attributes(self) -> None: + from nameparser.config import CONSTANTS + CONSTANTS.capitalize_name = True + CONSTANTS.force_mixed_case_capitalization = True + hn = HumanName('Shirley Maclaine') + self.assertEqual(str(hn), "Shirley MacLaine") + + def test_quote_nickname_formating(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} '{nickname}'" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III 'Kenny'") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III 'Kenny'") hn.string_format = "{last}, {title} {first} {middle}, {suffix} '{nickname}'" - self.assertEqual(u(hn), "Doe, Rev John A. Kenneth, III 'Kenny'") + self.assertEqual(str(hn), "Doe, Rev John A. Kenneth, III 'Kenny'") - def test_formating_removing_keys_from_format_string(self): + def test_formating_removing_keys_from_format_string(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} '{nickname}'" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III 'Kenny'") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III 'Kenny'") hn.string_format = "{last}, {title} {first} {middle}, {suffix}" - self.assertEqual(u(hn), "Doe, Rev John A. Kenneth, III") + self.assertEqual(str(hn), "Doe, Rev John A. Kenneth, III") hn.string_format = "{last}, {title} {first} {middle}" - self.assertEqual(u(hn), "Doe, Rev John A. Kenneth") + self.assertEqual(str(hn), "Doe, Rev John A. Kenneth") hn.string_format = "{last}, {first} {middle}" - self.assertEqual(u(hn), "Doe, John A. Kenneth") + self.assertEqual(str(hn), "Doe, John A. Kenneth") hn.string_format = "{last}, {first}" - self.assertEqual(u(hn), "Doe, John") + self.assertEqual(str(hn), "Doe, John") hn.string_format = "{first} {last}" - self.assertEqual(u(hn), "John Doe") + self.assertEqual(str(hn), "John Doe") - def test_formating_removing_pieces_from_name_buckets(self): + def test_formating_removing_pieces_from_name_buckets(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} '{nickname}'" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III 'Kenny'") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III 'Kenny'") hn.string_format = "{title} {first} {middle} {last} {suffix}" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III") - hn.middle='' - self.assertEqual(u(hn), "Rev John Doe III") - hn.suffix='' - self.assertEqual(u(hn), "Rev John Doe") - hn.title='' - self.assertEqual(u(hn), "John Doe") - - def test_formating_of_nicknames_with_parenthesis(self): + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III") + hn.middle = '' + self.assertEqual(str(hn), "Rev John Doe III") + hn.suffix = '' + self.assertEqual(str(hn), "Rev John Doe") + hn.title = '' + self.assertEqual(str(hn), "John Doe") + + def test_formating_of_nicknames_with_parenthesis(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} ({nickname})" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III (Kenny)") - hn.nickname='' - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III (Kenny)") + hn.nickname = '' + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III") - def test_formating_of_nicknames_with_single_quotes(self): + def test_formating_of_nicknames_with_single_quotes(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} '{nickname}'" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III 'Kenny'") - hn.nickname='' - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III 'Kenny'") + hn.nickname = '' + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III") - def test_formating_of_nicknames_with_double_quotes(self): + def test_formating_of_nicknames_with_double_quotes(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} {middle} {last} {suffix} \"{nickname}\"" - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III \"Kenny\"") - hn.nickname='' - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III") + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III \"Kenny\"") + hn.nickname = '' + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III") - def test_formating_of_nicknames_in_middle(self): + def test_formating_of_nicknames_in_middle(self) -> None: hn = HumanName("Rev John A. Kenneth Doe III (Kenny)") hn.string_format = "{title} {first} ({nickname}) {middle} {last} {suffix}" - self.assertEqual(u(hn), "Rev John (Kenny) A. Kenneth Doe III") - hn.nickname='' - self.assertEqual(u(hn), "Rev John A. Kenneth Doe III") - - def test_remove_emojis(self): + self.assertEqual(str(hn), "Rev John (Kenny) A. Kenneth Doe III") + hn.nickname = '' + self.assertEqual(str(hn), "Rev John A. Kenneth Doe III") + + def test_remove_emojis(self) -> None: hn = HumanName("Sam Smith 😊") - self.m(hn.first,"Sam", hn) - self.m(hn.last,"Smith", hn) - self.assertEqual(u(hn), "Sam Smith") + self.m(hn.first, "Sam", hn) + self.m(hn.last, "Smith", hn) + self.assertEqual(str(hn), "Sam Smith") - def test_keep_non_emojis(self): + def test_keep_non_emojis(self) -> None: hn = HumanName("∫≜⩕ Smith 😊") - self.m(hn.first,"∫≜⩕", hn) - self.m(hn.last,"Smith", hn) - self.assertEqual(u(hn), "∫≜⩕ Smith") + self.m(hn.first, "∫≜⩕", hn) + self.m(hn.last, "Smith", hn) + self.assertEqual(str(hn), "∫≜⩕ Smith") - def test_keep_emojis(self): + def test_keep_emojis(self) -> None: from nameparser.config import Constants constants = Constants() constants.regexes.emoji = False hn = HumanName("∫≜⩕ Smith😊", constants) - self.m(hn.first,"∫≜⩕", hn) - self.m(hn.last,"Smith😊", hn) - self.assertEqual(u(hn), "∫≜⩕ Smith😊") + self.m(hn.first, "∫≜⩕", hn) + self.m(hn.last, "Smith😊", hn) + self.assertEqual(str(hn), "∫≜⩕ Smith😊") # test cleanup + +class InitialsTestCase(HumanNameTestBase): + def test_initials(self) -> None: + hn = HumanName("Andrew Boris Petersen") + self.m(hn.initials(), "A. B. P.", hn) + + def test_initials_simple_name(self) -> None: + hn = HumanName("John Doe") + self.m(hn.initials(), "J. D.", hn) + hn = HumanName("John Doe", initials_format="{first} {last}") + self.m(hn.initials(), "J. D.", hn) + hn = HumanName("John Doe", initials_format="{last}") + self.m(hn.initials(), "D.", hn) + hn = HumanName("John Doe", initials_format="{first}") + self.m(hn.initials(), "J.", hn) + hn = HumanName("John Doe", initials_format="{middle}") + self.m(hn.initials(), "", hn) + + def test_initials_complex_name(self) -> None: + hn = HumanName("Doe, John A. Kenneth, Jr.") + self.m(hn.initials(), "J. A. K. D.", hn) + + def test_initials_format(self) -> None: + hn = HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first} {middle}") + self.m(hn.initials(), "J. A. K.", hn) + hn = HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first} {last}") + self.m(hn.initials(), "J. D.", hn) + hn = HumanName("Doe, John A. Kenneth, Jr.", initials_format="{middle} {last}") + self.m(hn.initials(), "A. K. D.", hn) + hn = HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first}, {last}") + self.m(hn.initials(), "J., D.", hn) + + def test_initials_format_constants(self) -> None: + from nameparser.config import CONSTANTS + _orig = CONSTANTS.initials_format + CONSTANTS.initials_format = "{first} {last}" + hn = HumanName("Doe, John A. Kenneth, Jr.") + self.m(hn.initials(), "J. D.", hn) + CONSTANTS.initials_format = "{first} {last}" + hn = HumanName("Doe, John A. Kenneth, Jr.") + self.m(hn.initials(), "J. D.", hn) + CONSTANTS.initials_format = _orig + + def test_initials_delimiter(self) -> None: + hn = HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";") + self.m(hn.initials(), "J; A; K; D;", hn) + + def test_initials_delimiter_constants(self) -> None: + from nameparser.config import CONSTANTS + _orig = CONSTANTS.initials_delimiter + CONSTANTS.initials_delimiter = ";" + hn = HumanName("Doe, John A. Kenneth, Jr.") + self.m(hn.initials(), "J; A; K; D;", hn) + CONSTANTS.initials_delimiter = _orig + + def test_initials_list(self) -> None: + hn = HumanName("Andrew Boris Petersen") + self.m(hn.initials_list(), ["A", "B", "P"], hn) + + def test_initials_list_complex_name(self) -> None: + hn = HumanName("Doe, John A. Kenneth, Jr.") + self.m(hn.initials_list(), ["J", "A", "K", "D"], hn) + + def test_initials_with_prefix_firstname(self) -> None: + hn = HumanName("Van Jeremy Johnson") + self.m(hn.initials_list(), ["V", "J", "J"], hn) + + def test_initials_with_prefix(self) -> None: + hn = HumanName("Alex van Johnson") + self.m(hn.initials_list(), ["A", "J"], hn) + + def test_constructor_first(self) -> None: + hn = HumanName(first="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.first, "TheName", hn) + + def test_constructor_middle(self) -> None: + hn = HumanName(middle="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.middle, "TheName", hn) + + def test_constructor_last(self) -> None: + hn = HumanName(last="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.last, "TheName", hn) + + def test_constructor_title(self) -> None: + hn = HumanName(title="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.title, "TheName", hn) + + def test_constructor_suffix(self) -> None: + hn = HumanName(suffix="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.suffix, "TheName", hn) + + def test_constructor_nickname(self) -> None: + hn = HumanName(nickname="TheName") + self.assertFalse(hn.unparsable) + self.m(hn.nickname, "TheName", hn) + + def test_constructor_multiple(self) -> None: + hn = HumanName(first="TheName", last="lastname", title="mytitle", full_name="donotparse") + self.assertFalse(hn.unparsable) + self.m(hn.first, "TheName", hn) + self.m(hn.last, "lastname", hn) + self.m(hn.title, "mytitle", hn) + + TEST_NAMES = ( "John Doe", "John Doe, Jr.", @@ -2147,7 +2572,9 @@ def test_keep_emojis(self): "Designated Judge David A. Ezra", "Sr US District Judge Richard G Kopf", "U.S. District Judge Marc Thomas Treadwell", - + "Dra. Andréia da Silva", + "Srta. Andréia da Silva", + ) @@ -2157,21 +2584,13 @@ class HumanNameVariationTests(HumanNameTestBase): TEST_NAMES = TEST_NAMES - def test_variations_of_TEST_NAMES(self): + def test_variations_of_TEST_NAMES(self) -> None: for name in self.TEST_NAMES: hn = HumanName(name) if len(hn.suffix_list) > 1: hn = HumanName("{title} {first} {middle} {last} {suffix}".format(**hn.as_dict()).split(',')[0]) - hn.C.empty_attribute_default = '' # format strings below require empty string + hn.C.empty_attribute_default = '' # format strings below require empty string hn_dict = hn.as_dict() - attrs = [ - 'title', - 'first', - 'middle', - 'last', - 'suffix', - 'nickname', - ] nocomma = HumanName("{title} {first} {middle} {last} {suffix}".format(**hn_dict)) lastnamecomma = HumanName("{last}, {title} {first} {middle} {suffix}".format(**hn_dict)) if hn.suffix: @@ -2194,11 +2613,12 @@ def test_variations_of_TEST_NAMES(self): if len(sys.argv) > 1: log.setLevel(logging.ERROR) log.addHandler(logging.StreamHandler()) - name = sys.argv[1] - hn = HumanName(name, encoding=sys.stdout.encoding) - print((repr(hn))) - hn.capitalize() - print((repr(hn))) + name_string = sys.argv[1] + hn_instance = HumanName(name_string, encoding=sys.stdout.encoding) + print(repr(hn_instance)) + hn_instance.capitalize() + print(repr(hn_instance)) + print("Initials: " + hn_instance.initials()) else: print("-"*80) print("Running tests") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a4818aa --- /dev/null +++ b/uv.lock @@ -0,0 +1,826 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, + { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nameparser" +source = { editable = "." } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dill" }, + { name = "mypy" }, + { name = "ruff" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] + +[package.metadata] +requires-dist = [{ name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.5.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "dill", specifier = ">=0.2.5" }, + { name = "mypy", specifier = ">=2.1" }, + { name = "ruff", specifier = ">=0.15" }, + { name = "sphinx", specifier = ">=8" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +]