Repository: antonagestam/phantom-types Branch: main Commit: 458040665f70 Files: 104 Total size: 293.5 KB Directory structure: gitextract_1ljscji0/ ├── .editorconfig ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── dependabot.yaml │ └── workflows/ │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .goose/ │ ├── check-manifest/ │ │ ├── manifest.json │ │ └── requirements.txt │ ├── node/ │ │ ├── manifest.json │ │ └── package.json │ └── python/ │ ├── manifest.json │ └── requirements.txt ├── .readthedocs.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── codecov.yml ├── docs/ │ ├── Makefile │ ├── conf.py │ ├── index.rst │ └── pages/ │ ├── composing-types.rst │ ├── external-wrappers.rst │ ├── functional-composition.rst │ ├── getting-started.rst │ ├── implementation.rst │ ├── predicates.rst │ ├── pydantic-support.rst │ └── types.rst ├── docs-requirements.txt ├── goose.yaml ├── mypy.ini ├── pyproject.toml ├── ruff.toml ├── setup.cfg ├── src/ │ └── phantom/ │ ├── __init__.py │ ├── _base.py │ ├── _hypothesis.py │ ├── _utils/ │ │ ├── __init__.py │ │ ├── misc.py │ │ └── types.py │ ├── boolean.py │ ├── bounds.py │ ├── datetime.py │ ├── errors.py │ ├── ext/ │ │ ├── __init__.py │ │ └── phonenumbers.py │ ├── fn.py │ ├── interval.py │ ├── iso3166.py │ ├── negated.py │ ├── predicates/ │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _utils.py │ │ ├── boolean.py │ │ ├── collection.py │ │ ├── datetime.py │ │ ├── generic.py │ │ ├── interval.py │ │ ├── numeric.py │ │ └── re.py │ ├── py.typed │ ├── re.py │ ├── schema.py │ └── sized.py ├── tests/ │ ├── __init__.py │ ├── ext/ │ │ ├── __init__.py │ │ ├── test_hypothesis.py │ │ ├── test_phonenumbers.py │ │ └── test_phonenumbers.yaml │ ├── predicates/ │ │ ├── __init__.py │ │ ├── test_boolean.py │ │ ├── test_collection.py │ │ ├── test_datetime.py │ │ ├── test_generic.py │ │ ├── test_interval.py │ │ ├── test_numeric.py │ │ ├── test_re.py │ │ ├── test_utils.py │ │ └── utils.py │ ├── pydantic/ │ │ ├── __init__.py │ │ ├── test_datetime.py │ │ └── test_schemas.py │ ├── test_base.py │ ├── test_boolean.py │ ├── test_datetime.py │ ├── test_datetime.yaml │ ├── test_fn.py │ ├── test_fn.yaml │ ├── test_intersection.yaml │ ├── test_interval.py │ ├── test_interval.yaml │ ├── test_iso3166.py │ ├── test_iso3166.yaml │ ├── test_negated.py │ ├── test_negated.yaml │ ├── test_re.py │ ├── test_re.yaml │ ├── test_sized.py │ ├── test_sized.yaml │ ├── test_utils.py │ └── types.py └── typing-requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = True [*] end_of_line = lf insert_final_newline = True indent_style = space indent_size = 4 trim_trailing_whitespace = True [*.py] charset = utf-8 [*.{yml,yaml,md,toml}] indent_size = 2 [Makefile] indent_style = tab ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Citizen Code of Conduct ## 1. Purpose A primary goal of Phantom Types is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. We invite all those who participate in Phantom Types to help us create safe and positive experiences for everyone. ## 2. Open [Source/Culture/Tech] Citizenship A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. ## 3. Expected Behavior The following behaviors are expected and requested of all community members: - Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. - Exercise consideration and respect in your speech and actions. - Attempt collaboration before conflict. - Refrain from demeaning, discriminatory, or harassing behavior and speech. - Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. - Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. ## 4. Unacceptable Behavior The following behaviors are considered harassment and are unacceptable within our community: - Violence, threats of violence or violent language directed against another person. - Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. - Posting or displaying sexually explicit or violent material. - Posting or threatening to post other people's personally identifying information ("doxing"). - Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. - Inappropriate photography or recording. - Inappropriate physical contact. You should have someone's consent before touching them. - Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. - Deliberate intimidation, stalking or following (online or in person). - Advocating for, or encouraging, any of the above behavior. - Sustained disruption of community events, including talks and presentations. ## 5. Weapons Policy No weapons will be allowed at Phantom Types events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. ## 6. Consequences of Unacceptable Behavior Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. Anyone asked to stop unacceptable behavior is expected to comply immediately. If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). ## 7. Addressing Grievances If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. ## 8. Scope We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. ## 9. License and attribution The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). _Revision 2.3. Posted 6 March 2017._ _Revision 2.2. Posted 4 February 2016._ _Revision 2.1. Posted 23 June 2014._ _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ ================================================ FILE: .github/CONTRIBUTING.md ================================================ ## Contributing Guidelines All sorts of contributions are welcome, ranging from raising ideas, reporting bugs, helping review open PRs, pointing out problematic design decisions et c, and of course by contributing code and documentation changes via pull requests. If your inquiry is less concrete, or you want to ask a question, please [feel free to start a new discussion][discussion]. There aren't any stupid questions, and if your inquiry has already been answered elsewhere, I'll do my best to direct you there. [discussion]: https://github.com/antonagestam/phantom-types/discussions I'll look at all PRs, but it's worth noting that the project has a high bar for merging contributions, and all features are expected to be well tested and documented. If your not sure what this means, it's totally OK to open draft PRs and ask questions. The [README][readme] has instructions for how to set up a development environment and testing code locally. [readme]: https://github.com/antonagestam/phantom-types/blob/main/README.md#development ================================================ FILE: .github/dependabot.yaml ================================================ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: monthly - package-ecosystem: github-actions directory: "/" schedule: interval: monthly ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: push: branches: - main pull_request: workflow_dispatch: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint: name: Static analysis uses: antonagestam/goose/.github/workflows/run.yaml@0.10.1 type-check: name: Type check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 cache: pip cache-dependency-path: typing-requirements.txt check-latest: true - name: mypy cache uses: actions/cache@v4 with: path: .mypy_cache key: "${{ runner.os }}-mypy-3.12-${{ hashFiles('typing-requirements.txt') }}" restore-keys: | ${{ runner.os }}-mypy-3.12 ${{ runner.os }}-mypy - run: pip install --require-hashes --no-dependencies -r typing-requirements.txt - run: pip install --no-dependencies . - run: mypy check-build: name: Check packaging metadata uses: less-action/reusables/.github/workflows/python-test-build.yaml@main docs: name: Build Sphinx Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: # Keep in sync with version in .readthedocs.yml. python-version: "3.11" cache: pip cache-dependency-path: docs-requirements.txt - name: Install dependencies run: pip install --require-hashes --no-dependencies -r docs-requirements.txt - name: Install package run: pip install --no-dependencies . - name: Build docs run: sphinx-build -W -b html docs docs/_build test: name: Test runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: pyproject.toml - name: Install minimum requirements run: pip install --upgrade '.[test]' - name: Run all tests that don't require extra dependencies run: >- coverage run --append -m pytest -m "no_external or not external" --ignore=src/phantom/ext --ignore=tests/pydantic --ignore=tests/ext - name: Install extra requirements run: pip install --upgrade '.[all,test]' - name: Run all tests that require extra dependencies run: >- coverage run --append -m pytest -m "external" - name: Collect coverage run: | coverage report coverage xml - name: Report coverage uses: codecov/codecov-action@v5 with: files: "coverage.xml" fail_ci_if_error: true name: codecov-py${{ matrix.python-version }} token: ${{ secrets.CODECOV_TOKEN }} verbose: true ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: release: types: [published] jobs: build-and-publish: name: Build and publish runs-on: ubuntu-latest permissions: # permission required for trusted publishing id-token: write environment: name: pypi url: https://pypi.org/p/immoney steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 cache: pip cache-dependency-path: pyproject.toml check-latest: true - name: Install dependencies run: python3 -m pip install --upgrade build pkginfo - name: Build run: python3 -m build --sdist --wheel . - name: Inspect built wheel version id: inspect-wheel-version run: | python3 << 'EOF' >> $GITHUB_OUTPUT from pathlib import Path from pkginfo import Wheel [wheel_path] = Path("dist").glob("*.whl") wheel = Wheel(wheel_path) print(f"version={wheel.version}") EOF - name: Fail on version mismatch if: ${{ steps.inspect-wheel-version.outputs.version != github.event.release.tag_name }} run: | echo "💥 The version of the built wheel does not match the release tag." echo echo "Release tag: '${{ github.event.release.tag_name }}'" echo "Packaged version: '${{ steps.inspect-wheel-version.outputs.version }}'" exit 1 - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ coverage.md # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Dynamic version file src/phantom/_version.py ================================================ FILE: .goose/check-manifest/manifest.json ================================================ {"source_ecosystem":{"language":"python","version":"3.13"},"source_dependencies":["check-manifest","setuptools-scm==8.3.1","setuptools==80.9.0","wheel==0.45.1"],"lock_files":[{"path":"requirements.txt","checksum":"sha256:b77b12f702e9b9cc47fb51a7b3d507e4f102209f4b4ff7982edc70f558ee14f6"}],"checksum":"sha256:76f610fd622fe0781ea867ed95ad9ef365f69148c7713661b62c92c4e05ba04a","ecosystem_version":"3.13.7"} ================================================ FILE: .goose/check-manifest/requirements.txt ================================================ build==1.3.0 \ --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 check-manifest==0.51 \ --hash=sha256:9801c7637675755a563f33e3c48ee59a59b37a7677297c05c910c16c5b9b6d67 \ --hash=sha256:f5f35ed561012fc2115bb070e42a748ac2e034cf8904ab4dfaae893859085ca4 packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c setuptools-scm==8.3.1 \ --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \ --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63 wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 ================================================ FILE: .goose/node/manifest.json ================================================ {"source_ecosystem":"node","source_dependencies":["prettier"],"lock_files":[{"path":"package-lock.json","checksum":"sha256:800168958486b7fda644290aed3749c91b62334f78e691cb6f4bd3c1e562c8dc"},{"path":"package.json","checksum":"sha256:3eae71af3c5bec28e9719c4cc2371efe5e4d149cf6317f46feb949148680d19f"}],"checksum":"sha256:6767446d081cac0f5cbd56c320e6952bf30741ca89498267c898088281a29694","ecosystem_version":"24.0.2"} ================================================ FILE: .goose/node/package.json ================================================ {"lockfileVersion":3,"dependencies":{"prettier":"*"}} ================================================ FILE: .goose/python/manifest.json ================================================ {"source_ecosystem":{"language":"python","version":"3.13"},"source_dependencies":["blacken-docs","check-jsonschema","editorconfig-checker","pre-commit-hooks","ruff"],"lock_files":[{"path":"requirements.txt","checksum":"sha256:c17077e77d3ce109826345fca16d1a5ca132f991f9297f99ad6514f5f5e8cd7d"}],"checksum":"sha256:3ad1408c2471b7ba9c2e1979543cca3a9d600822c7d79f15a5a9bd95b7d50ab3","ecosystem_version":"3.13.7"} ================================================ FILE: .goose/python/requirements.txt ================================================ attrs==25.4.0 \ --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 black==25.9.0 \ --hash=sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175 \ --hash=sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619 \ --hash=sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140 \ --hash=sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0 \ --hash=sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92 \ --hash=sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f \ --hash=sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa \ --hash=sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae \ --hash=sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a \ --hash=sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357 \ --hash=sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4 \ --hash=sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e \ --hash=sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d \ --hash=sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608 \ --hash=sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831 \ --hash=sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f \ --hash=sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7 \ --hash=sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1 \ --hash=sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823 \ --hash=sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933 \ --hash=sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47 \ --hash=sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713 blacken-docs==1.20.0 \ --hash=sha256:2d5b6caf6e7da5694b1eba97f9132c1ab9f14f221c82205ec473a6e74fbb2c6d \ --hash=sha256:a0d842811ee07802dec920d3cf831e21f6eb017712748b488489aa3688770f1e certifi==2025.10.5 \ --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 check-jsonschema==0.34.1 \ --hash=sha256:024ca6b1d645fdc33025f915ea4aa72a84df002b12209ee172a7bfd089f34832 \ --hash=sha256:39f3faea89a26e7de6c8090bc53dceecd30029a6daf04db7e5807a2142ef54b6 click==8.3.0 \ --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 editorconfig-checker==3.4.1 \ --hash=sha256:11468139bd1545b96a8ee54ed7dbc9f065082e0a0d6575a22050773bbcfed6c1 idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 jsonschema==4.25.1 \ --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pathspec==0.12.1 \ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 platformdirs==4.5.0 \ --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 pre-commit-hooks==6.0.0 \ --hash=sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2 \ --hash=sha256:76d8370c006f5026cdd638a397a678d26dda735a3c88137e05885a020f824034 pytokens==0.3.0 \ --hash=sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a \ --hash=sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3 referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 regress==2025.10.1 \ --hash=sha256:02da99e41c6a97f6d2146d326541b4035ed2139c92b2f56ca7e464ceb84fe24f \ --hash=sha256:05d02b4d7179b85acf28d7329a488901d6baef5f5b337dcd52f53ea0ff980bc3 \ --hash=sha256:05e6c68021dff89fee7bcdab25e0819cff4c7f0761dc41f0fb609b0e9ebb6272 \ --hash=sha256:0680ac0b0a0058acb55b64aa56956732cf56b0baa84ea95e3fa124ab16da58aa \ --hash=sha256:089b6c5f0965962e8046493e5a1222a24e88d6f235fa8fe2424ec869e6ff613c \ --hash=sha256:08e99c44e4c3860352400af96b25a4ebb673c16d53c6367153631ec77d5130b8 \ --hash=sha256:0d1f3c465f415521ca259209dd2587c9a35785b35e98d62d4a7fba3c2bf1cfc8 \ --hash=sha256:0ee12c4e1c4e6e3609f41dd58065bc241945e912772f7320238d6544f0745950 \ --hash=sha256:102c4627f026db8d361ab61155e0f1093176555d60ddb1cc4c9b6f5bbe255c1f \ --hash=sha256:12430b2c7263a7b359d30dd0f2b179426d489df30da78cd21023376eb2fe2682 \ --hash=sha256:1460d95d39d956ba0fab8a7b614b7ee5486473b1b210f65d6a3043dc08462f38 \ --hash=sha256:148f15530807a63e24ca2dea3795546723d84c0b3a8e4152a24b58318f841b3c \ --hash=sha256:15a7c2777e8eeb153fe87327871669c3bd1aab4fe20c1fccf448616bc298350d \ --hash=sha256:15efcb8d568e712919cb56b78fffe08a09d5ed1844b43cccc3235c2da90d0e59 \ --hash=sha256:1624855927d72bb0f8281785fc110eb89702078abe1c42e2e6cbe872ec374277 \ --hash=sha256:1809ac971207bb2e098a46e08c2f241a259ac3bda48228f295a3c2cba49c6c0e \ --hash=sha256:189899161133e7c56e733a3fc939642611585c5599626352ee0849cd34d7f436 \ --hash=sha256:19b875513ebecc2b37a125d5794ce91fb4c203b06cb187e15d13b2a761df4621 \ --hash=sha256:212fe8c4f823730d1578d0fe8b5edf21b393074b90a8ec704d6d6c1c96ffe7dc \ --hash=sha256:25d8518285aa3ce67ddbede90a1a9ca6a5d23a1b8275dd5a9722af0c64b37b2a \ --hash=sha256:287f86b5c0bf3bc9c0abd45bf6745ba9c6a5624c3132b07631bac4403b45143f \ --hash=sha256:28941c80252119ef051ad67195fa8d155a0c8dc9ecb801786d94eeea6738e4c3 \ --hash=sha256:290a15652c3fafe387db02061d4ed9a100804ed5a162d069b5a0ac28b5df162a \ --hash=sha256:293be370961c6887efb82e466a15523ff24a702d444f44917ce318b222ffd229 \ --hash=sha256:30e70b966015beef7e9287feaf37ed9606744ceaa88be24b10824f5b4c354498 \ --hash=sha256:337ef62f785dc6e05c4a09be7a902980cf4d4f15346f4eca9eaf02745485440c \ --hash=sha256:3605ef64ab64658856ce8d6fda730dfb62c01002e2283bd6400df8e912d1b56a \ --hash=sha256:389c2278848cbfed81753a04ce8ea6c037271179cb9ef4decac7d3c65ae3330b \ --hash=sha256:39600db4e404155168d6f75c3dbb2ae03ef3e288b4a57c7a6562a1144bf07682 \ --hash=sha256:3d12e6a834ed5f6d9dc7e86ea8fd77d37bb13900129930151d87c3261c3a799e \ --hash=sha256:3d48ae257483ff7da43c15b8071b132a1e75e4be2242a44e2587b8492afae32e \ --hash=sha256:415dd30885dfdb57f281f7c284e4495e85a90aa66965e3f10cb1bd862f40c2e4 \ --hash=sha256:45695cd7ddc6a919863f243a09a9e737257f958c0d2af0e71e349c9d0f3048ad \ --hash=sha256:469b03b1deffb6d20ea4f95aa44ef0e0e5a9b47d56f709276b06a96338b570a0 \ --hash=sha256:46b92e1ec6092e6e989e4ad5f52f0a358e88355f70cf4dba9abce84f3cd513cf \ --hash=sha256:47c03bfd853651241fc11436fb14ef7c9b312bb9a9c2828aa5c93943e945f1ef \ --hash=sha256:4c559282471f4f0c9bf7b588985c4bec16de0a85e1a3e017dca39640e15118ce \ --hash=sha256:4d0bf23a6d996655ed88c822bb0123cc2e92a1df95079ce7408552c35ec05d47 \ --hash=sha256:5541da1f581377bc20d2f77e017453aa8f2c2f4bfe7679dee00e139ec700abe8 \ --hash=sha256:56123dbe783a2bab04d1a1850605c483d40f36196bc52d249aa245d06f866f78 \ --hash=sha256:56ef2f8ef9a102a7d42cbbc2f3c0c5e1b186bd8eaa78d564da566b0bf20653dc \ --hash=sha256:590abc9fa10255dcf84b4469f08cad9787001c38600080a033cd7a71f51cae02 \ --hash=sha256:5a2ddb0d1c0821b70dd50daa773c5f3fbb2155398c57809a2f54447958c9569f \ --hash=sha256:5c147e4d3799022bac9fc49fd042a51b7f746c41ed231fa6b496720dab0d2f9d \ --hash=sha256:5d961e81b169fce4f6c0e35f52bbfca9e20abf9674dd391c75708f0665ef4f6f \ --hash=sha256:63503a8f601a10e5d9d72ea6efb415a2838a4766736775322578ce5fe18cb233 \ --hash=sha256:6553c8ba57fa92ab3e9ef5c811d6214c80131bba06496bd5920e6e5a3d53ca8e \ --hash=sha256:6b52096ecbf39f50756e51efa9286f47c598572f6b8bb2119f855de817f38b8e \ --hash=sha256:6c1c968ee4dca933e0cd6cea51d9c6494c3d82650ce826e7a5d007c9de720da2 \ --hash=sha256:6c4c40a8c9f3e0119d2384a52b55dcc770461e1ead6ae7b41314999223116a15 \ --hash=sha256:6d26daee7d46905c8d4232f44c39ea788f10d39c166735177f603783b4ace4e8 \ --hash=sha256:6d451a88292c5a93f57cf754c71b3aa8b570ed159f9fd481554948c467e6105d \ --hash=sha256:701d1e4f2b30abfb39b27759a52e68cab8b76484b3d9b51b2d7f368807613f77 \ --hash=sha256:707e2846e784ecef666a6892753cf5d8441e4cf02bcc7fa10fd21527429815fc \ --hash=sha256:70c78c6cf679da47fbde85b75407fdfb4642477315ee94d6cdd62a0606941b83 \ --hash=sha256:722c408a3bc92b4904005e68244c28fa6df943290df8d670faf349414c86aabb \ --hash=sha256:7343ef7eae795e1449308d6d05131195f5af31ab1b2b6b405ff9370b64c1e7e1 \ --hash=sha256:77d63b338c2a4e56b4f05632d3fd94061ad47ee2b272b158b9c2e09545a4c6bb \ --hash=sha256:783b9c50760aab988e4d60dcb7c54eba3fe730d80f9a877f1dc52d14263f86b1 \ --hash=sha256:7ac0e197c7f1b5ffca42341518b6a03e2ea3cdd66516af9278492d2d2bfc9ce2 \ --hash=sha256:8051fac3696730bb84d7675c62c7073792a0e105233d4f8e1055f2cab9b04fce \ --hash=sha256:81733f0e7f583d181bc9fc187b3d766489bcf7e0c85eff26f1e067c942e4e45a \ --hash=sha256:8405a31f0a1475e1c9aa20c4d6e1465ef2f7259581c018a2e273083494ca9a61 \ --hash=sha256:877e05e7c570ee1e077e8b587cca8a318b7675f3c94c6c4e25d0d145abf7c0b6 \ --hash=sha256:8a9c00ede347a5a431e36d4fcf75e2ec9094feac2f6170f2ffbd5e32f7f7a4b4 \ --hash=sha256:8d725e10a99ff65b0ad83b8c20f05645d9c67dbc809e2bf3b1db230d3e15081b \ --hash=sha256:8e337ce3b150ca0ff599b1150b995ff6a2e32b5940e17ac3f30a29133960709e \ --hash=sha256:8f42578d920fc878fe19ed8e2fbe38edd212c451bb2fc5e0084716d5acd26c4c \ --hash=sha256:905c4e364526f89b33db8e69468b15d9c294a56af4d533f2700e0e1a363fbae7 \ --hash=sha256:9100da34f69e18ad6d545f5d74f8ee729e42ce200a73752dd6d94f8a373d0e71 \ --hash=sha256:933561ea90b2ac9a826e956a994b8a66635cd96467374281da992ceea8b0de4c \ --hash=sha256:966dcad04fe1821c7af9bf6dd33bbeca5c2649b7a3c5b2af57700e6d93335fd4 \ --hash=sha256:9670d957d156fa90e5582c4337ca1757b643859780896c8d853b015cd01456dc \ --hash=sha256:97307f87b128389d8b3f385c8e431fc318263281d1a1c0394606bc813aea05fc \ --hash=sha256:9e25096697b034848d8fc7910cdb38b7abc2bd2d7ade8767893359918ed4efd0 \ --hash=sha256:9fdbbea49bf2fe65f7272b0316e1343aff1ccbb85b58fab325778c416d648ed9 \ --hash=sha256:a542e38c8bb95f618674a5d2d248f1010547d7ff2e46a6cf4fa4b851459ba440 \ --hash=sha256:a67216efa72bb27db170f637d67eb9acdc167da5d617163b057803de7aed1e6d \ --hash=sha256:a6f0320f53e8fd8722211c0620fcd9bfecc0db05bf0d59385ee301f86b815671 \ --hash=sha256:a97649f21c875b7e95e10a59f0f487a518735f51a7aec7fc95fb2c3d9a3914d5 \ --hash=sha256:aaf5d102e05b109dde13363e705969b3ffdbbfeb880270187eed0871e75f0c8f \ --hash=sha256:ac04243bf4ba86196b2491bdacd9450b339b3e5e97192aab82234baac1f0a74f \ --hash=sha256:adaa80c97927d623ff72b920bcc637568f124eabe84559c7927a91253ff55d5e \ --hash=sha256:adf331c8938e0d8705fc5d05d08fab09c11cb4bcdf8a64fa21902972c4cd38f4 \ --hash=sha256:af290cfb3b7d4b14319a88feab4e86f54a2c43bd8189be5a0dcd8f847f832847 \ --hash=sha256:afee501f000666afe18531132edcaf0dc0178dd591cccf5b9596563e7456c118 \ --hash=sha256:b4bc2008b59e5124c1672d7fd9f203e5d4a4ff88ebaf4666e3281141e2d8db20 \ --hash=sha256:b5254f758206ba45776aefbd3c4223898890209b4dd3a743f2dc5cd1ebc5e9fd \ --hash=sha256:b6b5aa9f9408fdf260c73071282a28d29efbb4c30a4b95ab29863bea31987621 \ --hash=sha256:b99a73cfbdc5d99681aeb3aeaa2d88369023c96648cc785433d6a92c8d3a8394 \ --hash=sha256:be96403b244e3d6925e225b4da5fbc4f5dd6f06b07f751f00047aa48fd2c7fe3 \ --hash=sha256:bf743b00cac20f8e8d271f275df7bc192ebb4faa7aee8fe98484df338786dec6 \ --hash=sha256:c0ed3b4d0df960aeccb685f8de5001c19f426f1ab09fde715e5abdbf9c59b26a \ --hash=sha256:c34b13e7785554997e18725b896d404ba992cee5b691768476f14b48de0c393a \ --hash=sha256:c35c8cd42900d9195e5bf48d701dda28c204854fba7cc27d18f309745f57b8b5 \ --hash=sha256:c3962e4b980bfb844518beb1a3afce069674377ba99189fe339918fe7e7cbb7a \ --hash=sha256:c7ecba827aff7f951db40be777c32608a1b16bbeb7f02fcc97a2e9fc6702641f \ --hash=sha256:cb23c4ab28e75e35f033e4e392f01f6a9344a961e2f4ded56af5520b5841fe8d \ --hash=sha256:cba77808a756f1f117916c0afb0e79f01be1cb46ee51c77780e8afc59ddac76b \ --hash=sha256:cbf1371ae2cfe1a2d6e1bb21f73a557138bc45347021f1701f196b484789639a \ --hash=sha256:ccfc11e2b632a21d82a56b52a70fc002b990fcf4f6b30661724732776bea6f1d \ --hash=sha256:ce4e38ada2a39159d8a2523084e2b5ed94b3f7f7b196ba6b99c2d6c4ff634a87 \ --hash=sha256:d0f63f4c3f2e701c832e54434ee10bd0bd2c850b0ae4029829d2dd0c9a340d83 \ --hash=sha256:d48e91d14a366570685f76adaf9d108e3abc5522572e7e1d0d78c3cc3ebf0833 \ --hash=sha256:daece9f2fbaaadd23ef2cb31ffbaecd74f946938ff9b70d22c891c66d1435ad2 \ --hash=sha256:dc9046f25971e9d6dc3b57028de8991d0d7c346efcb0c15acbfecbfb8e4c1813 \ --hash=sha256:dcc0a8af0cdbc3d6e0d4725f113335d0a5ffbba86ae3ca18d2b5b352c5f2c8ed \ --hash=sha256:de16665fcc008db88729f52bab92e36d0c0534bcb3f334ff6d2c7574b16a3d6d \ --hash=sha256:e2a0a7da1d42fdff8068ab976a66beea572621514a130b37592489ae134a0e27 \ --hash=sha256:e5c441a6017a5a29bb38c573892d882485cc26937cb1ee12da8593723bf6c041 \ --hash=sha256:e5f35e04fe6382c236d60c98b3f0a4a22dea75398b99c9c9bf3fb9d386cd7ebb \ --hash=sha256:e7cc153fabb47b6f8dfc2903186934e07aa57ee1debe9b3569ac4779b43708ae \ --hash=sha256:ee3b7325ea849097d674020c72b2e6deb8f1018f085a7ecbd22831cd217b7f80 \ --hash=sha256:f0599f9cb12722300b6d4fcd6bd9b2be5bf233bc567c3ca503d8e392de23798a \ --hash=sha256:f3afe24e6474f5dbc448f865641c29bcbed4eb3b87ac9eb0e6755c4eff4f7111 \ --hash=sha256:f418c11a6bae820fad1334e0569336e2e7848cf4b81e503bfb038daf0e57ec12 \ --hash=sha256:faa057895de8e41301f9367b286bb3fd0cd80bc8523c8c80866fe746a33d13b6 \ --hash=sha256:fb61e653a325aea4681cf7b96ba9bbabc1aeb3f3d8fe877a07800024907398b9 \ --hash=sha256:fdcd839a87fae1e47bf376248589ebcb2e58d3a0fad66837d69856ae99ef3c93 requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf rpds-py==0.28.0 \ --hash=sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296 \ --hash=sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1 \ --hash=sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04 \ --hash=sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d \ --hash=sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7 \ --hash=sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0 \ --hash=sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6 \ --hash=sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207 \ --hash=sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1 \ --hash=sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd \ --hash=sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec \ --hash=sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c \ --hash=sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7 \ --hash=sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c \ --hash=sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27 \ --hash=sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5 \ --hash=sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85 \ --hash=sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed \ --hash=sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d \ --hash=sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78 \ --hash=sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342 \ --hash=sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205 \ --hash=sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a \ --hash=sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db \ --hash=sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b \ --hash=sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27 \ --hash=sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120 \ --hash=sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527 \ --hash=sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592 \ --hash=sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa \ --hash=sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370 \ --hash=sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41 \ --hash=sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c \ --hash=sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f \ --hash=sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728 \ --hash=sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd \ --hash=sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e \ --hash=sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1 \ --hash=sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01 \ --hash=sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7 \ --hash=sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912 \ --hash=sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f \ --hash=sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8 \ --hash=sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72 \ --hash=sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a \ --hash=sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515 \ --hash=sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578 \ --hash=sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9 \ --hash=sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b \ --hash=sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829 \ --hash=sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37 \ --hash=sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a \ --hash=sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907 \ --hash=sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3 \ --hash=sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84 \ --hash=sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa \ --hash=sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733 \ --hash=sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f \ --hash=sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc \ --hash=sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d \ --hash=sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5 \ --hash=sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe \ --hash=sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e \ --hash=sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a \ --hash=sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399 \ --hash=sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316 \ --hash=sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c \ --hash=sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d \ --hash=sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d \ --hash=sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea \ --hash=sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2 \ --hash=sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a \ --hash=sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66 \ --hash=sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4 \ --hash=sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f \ --hash=sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa \ --hash=sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a \ --hash=sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c \ --hash=sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092 \ --hash=sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6 \ --hash=sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95 \ --hash=sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9 \ --hash=sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e \ --hash=sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712 \ --hash=sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91 \ --hash=sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d \ --hash=sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457 \ --hash=sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67 \ --hash=sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491 \ --hash=sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e \ --hash=sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08 \ --hash=sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724 \ --hash=sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259 \ --hash=sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424 \ --hash=sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb \ --hash=sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472 \ --hash=sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa \ --hash=sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e \ --hash=sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba \ --hash=sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c \ --hash=sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d \ --hash=sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b \ --hash=sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28 \ --hash=sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56 \ --hash=sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628 \ --hash=sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b \ --hash=sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a \ --hash=sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8 \ --hash=sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476 \ --hash=sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2 \ --hash=sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c \ --hash=sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba \ --hash=sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8 \ --hash=sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16 \ --hash=sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e ruamel-yaml==0.18.16 \ --hash=sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba \ --hash=sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a ruamel-yaml-clib==0.2.14 \ --hash=sha256:090782b5fb9d98df96509eecdbcaffd037d47389a89492320280d52f91330d78 \ --hash=sha256:0a54e5e40a7a691a426c2703b09b0d61a14294d25cfacc00631aa6f9c964df0d \ --hash=sha256:10d9595b6a19778f3269399eff6bab642608e5966183abc2adbe558a42d4efc9 \ --hash=sha256:16a60d69f4057ad9a92f3444e2367c08490daed6428291aa16cefb445c29b0e9 \ --hash=sha256:18c041b28f3456ddef1f1951d4492dbebe0f8114157c1b3c981a4611c2020792 \ --hash=sha256:1c1acc3a0209ea9042cc3cfc0790edd2eddd431a2ec3f8283d081e4d5018571e \ --hash=sha256:1f118b707eece8cf84ecbc3e3ec94d9db879d85ed608f95870d39b2d2efa5dca \ --hash=sha256:2070bf0ad1540d5c77a664de07ebcc45eebd1ddcab71a7a06f26936920692beb \ --hash=sha256:26a8de280ab0d22b6e3ec745b4a5a07151a0f74aad92dd76ab9c8d8d7087720d \ --hash=sha256:275f938692013a3883edbd848edde6d9f26825d65c9a2eb1db8baa1adc96a05d \ --hash=sha256:27c070cf3888e90d992be75dd47292ff9aa17dafd36492812a6a304a1aedc182 \ --hash=sha256:29757bdb7c142f9595cc1b62ec49a3d1c83fab9cef92db52b0ccebaad4eafb98 \ --hash=sha256:4ccba93c1e5a40af45b2f08e4591969fa4697eae951c708f3f83dcbf9f6c6bb1 \ --hash=sha256:4f4a150a737fccae13fb51234d41304ff2222e3b7d4c8e9428ed1a6ab48389b8 \ --hash=sha256:557df28dbccf79b152fe2d1b935f6063d9cc431199ea2b0e84892f35c03bb0ee \ --hash=sha256:5ac5ff9425d8acb8f59ac5b96bcb7fd3d272dc92d96a7c730025928ffcc88a7a \ --hash=sha256:5bae1a073ca4244620425cd3d3aa9746bde590992b98ee8c7c8be8c597ca0d4e \ --hash=sha256:5e56ac47260c0eed992789fa0b8efe43404a9adb608608631a948cee4fc2b052 \ --hash=sha256:6aeadc170090ff1889f0d2c3057557f9cd71f975f17535c26a5d37af98f19c27 \ --hash=sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352 \ --hash=sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83 \ --hash=sha256:7df6f6e9d0e33c7b1d435defb185095386c469109de723d514142632a7b9d07f \ --hash=sha256:7e4f9da7e7549946e02a6122dcad00b7c1168513acb1f8a726b1aaf504a99d32 \ --hash=sha256:803f5044b13602d58ea378576dd75aa759f52116a0232608e8fdada4da33752e \ --hash=sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad \ --hash=sha256:81f6d3b19bc703679a5705c6a16dabdc79823c71d791d73c65949be7f3012c02 \ --hash=sha256:83bbd8354f6abb3fdfb922d1ed47ad8d1db3ea72b0523dac8d07cdacfe1c0fcf \ --hash=sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6 \ --hash=sha256:915748cfc25b8cfd81b14d00f4bfdb2ab227a30d6d43459034533f4d1c207a2a \ --hash=sha256:94f3efb718f8f49b031f2071ec7a27dd20cbfe511b4dfd54ecee54c956da2b31 \ --hash=sha256:9bd8fe07f49c170e09d76773fb86ad9135e0beee44f36e1576a201b0676d3d1d \ --hash=sha256:9bf6b699223afe6c7fe9f2ef76e0bfa6dd892c21e94ce8c957478987ade76cd8 \ --hash=sha256:a05ba88adf3d7189a974b2de7a9d56731548d35dc0a822ec3dc669caa7019b29 \ --hash=sha256:a0ac90efbc7a77b0d796c03c8cc4e62fd710b3f1e4c32947713ef2ef52e09543 \ --hash=sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27 \ --hash=sha256:a37f40a859b503304dd740686359fcf541d6fb3ff7fc10f539af7f7150917c68 \ --hash=sha256:a911aa73588d9a8b08d662b9484bc0567949529824a55d3885b77e8dd62a127a \ --hash=sha256:aef953f3b8bd0b50bd52a2e52fb54a6a2171a1889d8dea4a5959d46c6624c451 \ --hash=sha256:b28caeaf3e670c08cb7e8de221266df8494c169bd6ed8875493fab45be9607a4 \ --hash=sha256:b30110b29484adc597df6bd92a37b90e63a8c152ca8136aad100a02f8ba6d1b6 \ --hash=sha256:b5b0f7e294700b615a3bcf6d28b26e6da94e8eba63b079f4ec92e9ba6c0d6b54 \ --hash=sha256:c099cafc1834d3c5dac305865d04235f7c21c167c8dd31ebc3d6bbc357e2f023 \ --hash=sha256:d73a0187718f6eec5b2f729b0f98e4603f7bd9c48aa65d01227d1a5dcdfbe9e8 \ --hash=sha256:d8354515ab62f95a07deaf7f845886cc50e2f345ceab240a3d2d09a9f7d77853 \ --hash=sha256:dba72975485f2b87b786075e18a6e5d07dc2b4d8973beb2732b9b2816f1bad70 \ --hash=sha256:dd7546c851e59c06197a7c651335755e74aa383a835878ca86d2c650c07a2f85 \ --hash=sha256:df3ec9959241d07bc261f4983d25a1205ff37703faf42b474f15d54d88b4f8c9 \ --hash=sha256:e1d1735d97fd8a48473af048739379975651fab186f8a25a9f683534e6904179 \ --hash=sha256:e501c096aa3889133d674605ebd018471bc404a59cbc17da3c5924421c54d97c \ --hash=sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640 \ --hash=sha256:f4e97a1cf0b7a30af9e1d9dad10a5671157b9acee790d9e26996391f49b965a2 \ --hash=sha256:f8b2acb0ffdd2ce8208accbec2dca4a06937d556fdcaefd6473ba1b5daa7e3c4 \ --hash=sha256:fb04c5650de6668b853623eceadcdb1a9f2fee381f5d7b6bc842ee7c239eeec4 \ --hash=sha256:fbc08c02e9b147a11dfcaa1ac8a83168b699863493e183f7c0c8b12850b7d259 \ --hash=sha256:ff86876889ea478b1381089e55cf9e345707b312beda4986f823e1d95e8c0f59 ruff==0.14.4 \ --hash=sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8 \ --hash=sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c \ --hash=sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5 \ --hash=sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349 \ --hash=sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff \ --hash=sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67 \ --hash=sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2 \ --hash=sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33 \ --hash=sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850 \ --hash=sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649 \ --hash=sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e \ --hash=sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde \ --hash=sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4 \ --hash=sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb \ --hash=sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518 \ --hash=sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469 \ --hash=sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3 \ --hash=sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5 \ --hash=sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132 urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc ================================================ FILE: .readthedocs.yml ================================================ # https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 sphinx: configuration: docs/conf.py build: os: ubuntu-22.04 tools: # Keep in sync with docs build CI job. python: "3.11" python: install: - requirements: docs-requirements.txt - method: pip path: . ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2020-2022, Anton Agestam All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ exclude Makefile exclude .editorconfig recursive-exclude docs * recursive-exclude .github * recursive-exclude .goose * recursive-exclude tests * recursive-include src py.typed exclude *.yaml exclude *.yml exclude *.toml include README.md include LICENSE include pyproject.toml exclude .gitignore exclude setup.cfg exclude mypy.ini exclude *requirements.txt ================================================ FILE: Makefile ================================================ SHELL := /usr/bin/env bash .PHONY: all # Currently running typeguard on all modules except: # - phantom.interval # - phantom._base # - phantom.ext.phonenumbers typeguard_packages := \ phantom.boolean \ phantom.datetime \ phantom.fn \ phantom.iso3166 \ phantom.re \ phantom.schema \ phantom.sized \ phantom.utils \ phantom.predicates._base \ phantom.predicates.boolean \ phantom.predicates.collection \ phantom.predicates.datetime \ phantom.prediactes.generic \ phantom.predicates.interval \ phantom.predicates.numeric \ phantom.predicates.re \ phantom.predicates.utils typeguard_arg := \ --typeguard-packages=$(shell echo $(typeguard_packages) | sed 's/ /,/g') .PHONY: test test: pytest $(test) -m 'not no_external' .PHONY: test-runtime test-runtime: pytest $(test) -k.py -m 'not no_external' .PHONY: test-typeguard test-typeguard: pytest $(typeguard_arg) $(test) .PHONY: test-typing test-typing: pytest $(test) -k.yaml .PHONY: clean clean: rm -rf {**/,}*.egg-info **{/**,}/__pycache__ build dist .coverage coverage.xml .PHONY: docs-requirements docs-requirements: export UV_CUSTOM_COMPILE_COMMAND='make docs-requirements' docs-requirements: @uv pip compile --generate-hashes --strip-extras --extra=docs --upgrade --output-file=docs-requirements.txt pyproject.toml .PHONY: docs-requirements typing-requirements: export UV_CUSTOM_COMPILE_COMMAND='make typing-requirements' typing-requirements: @uv pip compile --generate-hashes --strip-extras --extra=type-check --upgrade --output-file=typing-requirements.txt pyproject.toml ================================================ FILE: README.md ================================================

phantom-types

CI Build Status Documentation Build Status Test coverage report
PyPI Package Python versions

[Phantom types][ghosts] for Python will help you make illegal states unrepresentable and avoid shotgun parsing by enabling you to practice ["Parse, don't validate"][parse].

Checkout the complete documentation on Read the Docs →

## Installation ```bash $ python3 -m pip install phantom-types ``` #### Extras There are a few extras available that can be used to either enable a feature or install a compatible version of a third-party library. | Extra name | Feature | | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `[dateutil]` | Installs [python-dateutil]. Required for parsing strings with [`TZAware` and `TZNaive`][phantom-datetime]. | | `[phonenumbers]` | Installs [phonenumbers]. Required to use [`phantom.ext.phonenumbers`][phantom-phonenumbers]. | | `[pydantic]` | Installs [pydantic]. | | `[hypothesis]` | Installs [hypothesis]. | | `[all]` | Installs all of the above. | [python-dateutil]: https://pypi.org/project/python-dateutil/ [phonenumbers]: https://pypi.org/project/phonenumbers/ [pydantic]: https://pypi.org/project/pydantic/ [hypothesis]: https://pypi.org/project/hypothesis/ [phantom-datetime]: https://phantom-types.readthedocs.io/en/main/pages/types.html#module-phantom.datetime [phantom-phonenumbers]: https://phantom-types.readthedocs.io/en/main/pages/external-wrappers.html#module-phantom.ext.phonenumbers ```bash $ python3 -m pip install phantom-types[all] ``` ## Examples By introducing a phantom type we can define a pre-condition for a function argument. ```python from phantom import Phantom from phantom.predicates.collection import contained class Name(str, Phantom, predicate=contained({"Jane", "Joe"})): ... def greet(name: Name): print(f"Hello {name}!") ``` Now this will be a valid call. ```python greet(Name.parse("Jane")) ``` ... and so will this. ```python joe = "Joe" assert isinstance(joe, Name) greet(joe) ``` But this will yield a static type checking error. ```python greet("bird") ``` To be clear, the reason the first example passes is not because the type checker somehow magically knows about our predicate, but because we provided the type checker with proof through the `assert`. All the type checker cares about is that runtime cannot continue executing past the assertion, unless the variable is a `Name`. If we move the calls around like in the example below, the type checker would give an error for the `greet()` call. ```python joe = "Joe" greet(joe) assert isinstance(joe, Name) ``` ### Runtime type checking By combining phantom types with a runtime type-checker like [beartype] or [typeguard], we can achieve the same level of security as you'd gain from using [contracts][dbc]. ```python import datetime from beartype import beartype from phantom.datetime import TZAware @beartype def soon(dt: TZAware) -> TZAware: return dt + datetime.timedelta(seconds=10) ``` The `soon` function will now validate that both its argument and return value is timezone aware, e.g. pre- and post conditions. ### Pydantic support Phantom types are ready to use with [pydantic] and have [integrated support][pydantic-support] out-of-the-box. Subclasses of `Phantom` work with both pydantic's validation and its schema generation. ```python class Name(str, Phantom, predicate=contained({"Jane", "Joe"})): @classmethod def __schema__(cls) -> Schema: return super().__schema__() | { "description": "Either Jane or Joe", "format": "custom-name", } class Person(BaseModel): name: Name created: TZAware print(json.dumps(Person.schema(), indent=2)) ``` The code above outputs the following JSONSchema. ```json { "title": "Person", "type": "object", "properties": { "name": { "title": "Name", "description": "Either Jane or Joe", "format": "custom-name", "type": "string" }, "created": { "title": "TZAware", "description": "A date-time with timezone data.", "type": "string", "format": "date-time" } }, "required": ["name", "created"] } ``` ## Development Install development requirements, preferably in a virtualenv: ```bash $ python3 -m pip install .[all,test,type-check] ``` Run tests: ```bash $ pytest # or $ make test ``` Run type checker: ```bash $ mypy ``` Linters and formatters are set up with [goose], after installing it you can run it as: ```bash # run all checks $ goose run --select=all # or just a single hook $ goose run mypy --select=all ``` In addition to static type checking, the project is set up with [pytest-mypy-plugins] to test that exposed mypy types work as expected, these checks will run together with the rest of the test suite, but you can single them out with the following command. ```bash $ make test-typing ``` [parse]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ [ghosts]: https://kataskeue.com/gdp.pdf [build-status]: https://github.com/antonagestam/phantom-types/actions?query=workflow%3ACI+branch%3Amain [coverage]: https://codecov.io/gh/antonagestam/phantom-types [typeguard]: https://github.com/agronholm/typeguard [beartype]: https://github.com/beartype/beartype [dbc]: https://en.wikipedia.org/wiki/Design_by_contract [pydantic]: https://pydantic-docs.helpmanual.io/ [pydantic-support]: https://phantom-types.readthedocs.io/en/stable/pages/pydantic-support.html [goose]: https://github.com/antonagestam/goose [pytest-mypy-plugins]: https://github.com/TypedDjango/pytest-mypy-plugins ================================================ FILE: codecov.yml ================================================ comment: require_changes: true ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/conf.py ================================================ # This file only contains a selection of the most common options. For a full list see # the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import pathlib import sys # If extensions (or modules to document with autodoc) are in another directory, add # these directories to sys.path here. If the directory is relative to the documentation # root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("../src")) import phantom current_dir = pathlib.Path(__file__).resolve().parent def get_copyright_from_license() -> str: license_path = current_dir.parent / "LICENSE" prefix = "Copyright (c) " for line in license_path.read_text().split("\n"): if line.startswith(prefix): return line[len(prefix) :] raise RuntimeError("Couldn't parse copyright from LICENSE") # Project information project = "phantom-types" copyright = get_copyright_from_license() # noqa: A001 author = "Anton Agestam" version = phantom.__version__ release = version # Add any Sphinx extension module names here, as strings. They can be extensions coming # with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.autosummary", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] # # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and directories to # ignore when looking for source files. This pattern also affects html_static_path and # html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The theme to use for HTML and HTML Help pages. html_theme = "furo" # Set typing.TYPE_CHECKING to True to enable "expensive" typing imports. set_type_checking_flag = True typehints_fully_qualified = True always_document_param_types = True # Add any paths that contain custom static files (such as style sheets) here, relative # to this directory. They are copied after the builtin static files, so a file named # "default.css" will overwrite the builtin "default.css". # html_static_path = [] # Keep source order instead of sorting members alphabetically. autodoc_member_order = "bysource" ================================================ FILE: docs/index.rst ================================================ phantom-types ============= This is the documentation of phantom-types, a library that let's you arbitrarily narrow builtin types, without any runtime overhead. Full documentation and reference is available here, you will hopefully find what you're looking for in the sections below. Documentation sections ---------------------- .. toctree:: :maxdepth: 1 pages/getting-started.rst pages/types.rst pages/predicates.rst pages/composing-types.rst pages/functional-composition.rst pages/external-wrappers.rst pages/pydantic-support.rst pages/implementation.rst ================================================ FILE: docs/pages/composing-types.rst ================================================ .. _composing: Composing types *************** Bounds ====== The bound of a phantom type is the type that its values will have at runtime, so when checking if a value is an instance of a phantom type, it's first checked to be within its bounds, so that the value can be safely passed as argument to the predicate function of the type. When subclassing, the bound of the new type must be a subtype of the bound of the super class. The bound of a phantom type is exposed as :attr:`phantom.Phantom.__bound__` for introspection. Resolution order ~~~~~~~~~~~~~~~~ The bound of a phantom type is resolved in the order: explicitly by class argument, implicitly by base classes, or implicitly inheritance, e.g.: .. code-block:: python # Resolved by an explicit class arg: class A(Phantom, bound=str, predicate=...): ... # Resolved implicitly as any base classes before Phantom: class B(str, Phantom, predicate=...): ... # Resolves to str by inheritance from B: class C(B): ... Abstract bounds ~~~~~~~~~~~~~~~ It's sometimes useful to create base classes without specifying a bound type. To do so the class can be made abstract by passing ``abstract=True`` as a class argument: .. code-block:: python class Base(Phantom, abstract=True): ... class Concrete(str, Base): ... This is for instance used by the shipped :ref:`numeric interval types `. Bound erasure ~~~~~~~~~~~~~ If a phantom type doesn't properly specify its bounds, in addition to risking passing invalid arguments to its predicate function, it is also likely that a static type checker might inadvertently erase the runtime type when type guarding. As an example, this code will error on the access to ``dt.year`` because ``UTCDateTime.parse()`` has made the type checker erase the knowledge that dt is a ``datetime``. .. code-block:: python class UTCDateTime(Phantom, predicate=is_utc): ... dt = UTCDateTime.parse(now()) dt.year # Error! In this example we could remedy this by adding ``datetime`` as a base class and bound. .. code-block:: python class UTCDateTime(datetime.datetime, Phantom, predicate=is_utc): ... Mutability ========== Phantom types are completely incompatible with mutable data and should never be used to narrow a mutable type. The reason is that there is no way for a type checker to detect that a mutation changes an object to no longer satisfy the predicate of a phantom type. For example: .. code-block:: python class Mutable: def __init__(self, len: int): self.len = len def __len__(self) -> int: return self.len # A phantom type that checks that a list has more than 2 items. class HasMany(Mutable, Phantom, predicate=count(greater(2))): ... # The check will pass because the instantiated object *currently* satisfies the # predicate, e.g. has len() > 2. instance = HasMany.parse(Mutable(3)) # But! The object is mutable, so nothing is stopping us from altering it's length. # At this point the object will no longer satisfy the HasMany predicate. instance.len = 2 # There is no way for a type checker to know that the predicate isn't fulfilled # anymore, so the revealed type here will still be HasMany. reveal_type(instance) # Revealed type is HasMany When subclassing from :py:class:`Phantom `, a check is made that raises :py:class:`MutableType ` for known mutable types, such as :py:class:`list`, :py:class:`set`, :py:class:`dict` and unfrozen dataclasses. In the general case though, it isn't possible to detect mutability and so it's up to developer discipline to make sure not to mix mutable data types with phantom types. Metaclass conflicts =================== Phantom types are implemented using a metaclass. When creating a phantom type that narrows on a type that also uses a metaclass it's common to stumble into a metaclass conflict. The usual solution to such situation is to create a new metaclass that inherits both existing metaclasses and base the new type on it. .. code-block:: python from phantom import PhantomMeta class NewMeta(PhantomMeta, OldMeta): ... class New(Old, Phantom, metaclass=NewMeta): ... ================================================ FILE: docs/pages/external-wrappers.rst ================================================ External wrappers ================= A collection of phantom types that wraps functionality of well maintained implementations of third-party validation libraries. Importing from ``phantom.ext.*`` should be a hint that more dependencies need to be installed. Phone numbers ------------- .. automodule:: phantom.ext.phonenumbers Types ^^^^^ .. autoclass:: phantom.ext.phonenumbers.PhoneNumber :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.ext.phonenumbers.FormattedPhoneNumber :members: :undoc-members: :show-inheritance: Functions ^^^^^^^^^ .. autofunction:: phantom.ext.phonenumbers.is_phone_number .. autofunction:: phantom.ext.phonenumbers.is_formatted_phone_number .. autofunction:: phantom.ext.phonenumbers.normalize_phone_number Exceptions ^^^^^^^^^^ .. autoexception:: phantom.ext.phonenumbers.InvalidPhoneNumber :show-inheritance: ================================================ FILE: docs/pages/functional-composition.rst ================================================ Functional composition ====================== When composing predicates for phantom types it won't take long before you reach after functional composition. Unfortunately, fully supporting a typed n-ary ``compose`` functions isn't yet feasible without shipping a custom mypy plugin. In lieu of having that effort in place, a simpler but fully typed :py:func:`compose2 ` function is shipped. The door, however, is left wide open for the possibility of shipping an n-ary, generalized compose function in the future. .. automodule:: phantom.fn :members: :undoc-members: :show-inheritance: ================================================ FILE: docs/pages/getting-started.rst ================================================ Getting Started =============== Creating phantom types ---------------------- Phantom types are created by subclassing :py:class:`Phantom ` and providing a predicate function. .. code-block:: python from phantom import Phantom # A boolean predicate that checks if a given string is a greeting. This function is # of type ``Predicate[str]`` as it requires its argument to be a ``str``. def is_greeting(instance: str) -> bool: return instance.startswith(("Hello", "Hi")) # Since our predicate requires its argument to be a ``str``, we must make the bound # of the phantom type ``str`` as well. We do that by making it it's first base. Any # base specified before Phantom is implicitly interpreted as its bound, unless an # explicit bound is specified as a class argument. class Greeting(str, Phantom, predicate=is_greeting): ... hello = "Hello there" # We can narrow types using mypy's type guards assert isinstance(hello, Greeting) # or explicitly when we need to hi = Greeting.parse("Hi there") # The runtime types are unchanged and will still be str for our greetings assert type(hello) is str assert type(hi) is str # But their static types will be Greeting, retaining the information that our # strings are not just any strs if TYPE_CHECKING: reveal_type(hello) reveal_type(hi) # As this string doesn't fulfill our __instancecheck__, it will not be an # instance of Greeting. assert not isinstance("Goodbye", Greeting) A motivating example -------------------- Imagine that you're working on implementing a ``head()`` function that should return the first item of any given iterable. You start out with a simple implementation: .. code-block:: python def head(iterable: Iterable[T]) -> T: return next(iter(iterable)) You go ahead and use this function across your project, until suddenly you run into a subtle issue that you didn't think of: this function raises ``StopIteration`` when passed an empty iterable. In functional programming terms this is due to the function being *partial* it specifies that it takes ``Iterable`` as argument, but in reality we would need a narrower type to describe the set of valid arguments, and make the function *total*. You need to deal with the problem at hand so you go ahead and adjust all the call sites of your function, and you now end up either asserting that the iterables are non-empty, or catching the `StopIteration`. .. code-block:: python items = get_values() if not len(items): return "empty" return f"first element is: {head(items)}" This works, and you could move on like this from here, but, you have now introduced shotgun parsing into your application, since further down the processing line you need to check the length if the iterable for other purposes. Shotgun parsing is an anti-pattern that results in a program state that is hard to predict and will very likely lead to bugs down the line. So how should you deal with this? Using phantom types you can use the builtin :class:`phantom.sized.NonEmpty` type. .. code-block:: python def head(iterable: NonEmpty[T]) -> T: return next(iter(iterable)) The implementation is identical but you've now altered the signature of the function so that it's total, it can deal with *all* values of its argument type without raising an exception. By using the narrower type at the call sites, you avoid shotgun parsing, since the other logic further down in the processing chain can rely on the type as well, and you won't need to check the length of the iterable again. .. code-block:: python items = get_values() if not isinstance(items, NonEmpty): return "empty" return f"first element is: {head(items)}" This strategy works in all places where a function works on a narrower type than you can describe with the builtin types of Python, not only this made-up example. You can narrow strings, integers, datetimes, and any other arbitrary types to completely rid of duplicated validation throughout a code base. There's a set of phantom types that ships builtin that are helpful to build on top of, although you might mostly use your own custom phantom types that describe the exact values that your implementations require. Using predicates ---------------- The phantom-types library relies heavily on boolean predicates. A boolean predicate is simply a function that takes a single argument and returns either ``True`` or ``False``. While using boolean predicates is not necessary to create phantom types, building up a library of types doing so allows reusing small and easily testable functions to create a plethora of specialized types. Boolean predicates are usually easy to reason about as they are pure functions with only two possible return values. Studying the phantom types shipped in this library is recommended for gaining deeper insight into how to implement more complicated types. Next steps ---------- - Check out the :ref:`builtin phantom types ` that is shipped with the library. - Check out the basis of :ref:`predicates and predicate` factories to build phantom types from. - Read more in-depth about :ref:`composing phantom types `. ================================================ FILE: docs/pages/implementation.rst ================================================ Implementation ============== How are phantom types implemented? ---------------------------------- phantom-types make use of Python's ``__instancecheck__`` protocol to make types work with the same checks that are recognized as type guards by static type checkers, e.g. ``isinstance()``. Phantom types are never instantiated at runtime and so will not add any processing-, or memory overhead. Instead the question of whether a value is properly parsed before it is processed is deferred to the static type checker. The choice to design the library around boolean predicates, and the fact that much of the initially shipped builtin types use predicates, are heavily inspired by the `fthomas/refined `_ library. ================================================ FILE: docs/pages/predicates.rst ================================================ .. _predicates: Predicates and factories ======================== Predicates are functions that return a boolean value given a single argument. These modules contain predicate functions, and functions that return predicates, that can be composed and used for phantom types. Boolean logic ------------- .. automodule:: phantom.predicates.boolean :members: :undoc-members: Collection ---------- .. automodule:: phantom.predicates.collection :members: :undoc-members: Datetime -------- .. automodule:: phantom.predicates.datetime :members: :undoc-members: Generic ------- .. automodule:: phantom.predicates.generic :members: Numeric intervals ----------------- .. automodule:: phantom.predicates.interval :members: :undoc-members: Numeric ------- .. automodule:: phantom.predicates.numeric :members: :undoc-members: Regular expressions ------------------- .. automodule:: phantom.predicates.re :members: :undoc-members: ================================================ FILE: docs/pages/pydantic-support.rst ================================================ Pydantic Support ================ phantom-types supports pydantic_ out of the box by providing a :func:`__get_validators__() ` hook on the base :class:`Phantom ` class. Most of the shipped types also implements full JSON Schema and OpenAPI support. .. _pydantic: https://pydantic-docs.helpmanual.io/ To make a phantom type compatible with pydantic, all you need to do is override :func:`Phantom.__schema__() `: .. code-block:: python from phantom import Phantom from phantom.schema import Schema class Name(str, Phantom, predicate=...): @classmethod def __schema__(cls) -> Schema: return super().__schema__() | Schema( description="A type for names", format="name-format", ) As can be seen in the example, ``__schema__()`` implementations are expected to return a dict extending its ``super().__schema__()``, however this is not a requirement and any :class:`Schema `-compatible ``dict`` can be returned. ================================================ FILE: docs/pages/types.rst ================================================ .. _types: Types ===== Base classes ------------ .. automodule:: phantom .. autoclass:: Phantom :members: :undoc-members: :show-inheritance: :inherited-members: :special-members: __schema__, __modify_schema__, __get_validators__, __bound__ Boolean ------- .. automodule:: phantom.boolean :members: :undoc-members: :show-inheritance: Country codes ------------- .. automodule:: phantom.iso3166 :members: :undoc-members: :show-inheritance: Datetime -------- .. automodule:: phantom.datetime :members: :undoc-members: :show-inheritance: Negated types ------------- .. automodule:: phantom.negated :members: :undoc-members: :show-inheritance: .. _numeric-intervals: Numeric intervals ----------------- .. automodule:: phantom.interval Base classes ^^^^^^^^^^^^ .. autoclass:: phantom.interval.Interval :members: __check__ :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.Exclusive :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.Inclusive :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.ExclusiveInclusive :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.InclusiveExclusive :members: :undoc-members: :show-inheritance: Concrete interval types ^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: phantom.interval.Natural :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.NegativeInt :members: :undoc-members: :show-inheritance: .. autoclass:: phantom.interval.Portion :members: :undoc-members: :show-inheritance: Regular expressions ------------------- .. automodule:: phantom.re :members: :undoc-members: :show-inheritance: Sized collections ----------------- .. automodule:: phantom.sized :members: :undoc-members: :show-inheritance: ================================================ FILE: docs-requirements.txt ================================================ # This file was autogenerated by uv via the following command: # 'make docs-requirements' accessible-pygments==0.0.5 \ --hash=sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872 \ --hash=sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7 # via furo alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx babel==2.17.0 \ --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 # via sphinx beartype==0.22.5 \ --hash=sha256:516a9096cc77103c96153474fa35c3ebcd9d36bd2ec8d0e3a43307ced0fa6341 \ --hash=sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0 # via numerary beautifulsoup4==4.14.2 \ --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 # via furo certifi==2025.10.5 \ --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 # via requests charset-normalizer==3.4.4 \ --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 # via requests docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via sphinx furo==2025.9.25 \ --hash=sha256:2937f68e823b8e37b410c972c371bc2b1d88026709534927158e0cb3fac95afe \ --hash=sha256:3eac05582768fdbbc2bdfa1cdbcdd5d33cfc8b4bd2051729ff4e026a1d7e0a98 # via phantom-types (pyproject.toml) hypothesis==6.147.0 \ --hash=sha256:72e6004ea3bd1460bdb4640b6389df23b87ba7a4851893fd84d1375635d3e507 \ --hash=sha256:de588807b6da33550d32f47bcd42b1a86d061df85673aa73e6443680249d185e # via phantom-types (pyproject.toml) idna==3.11 \ --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 # via requests imagesize==1.4.1 \ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a # via sphinx jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via sphinx markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 numerary==0.4.4 \ --hash=sha256:ad955ddf7f5275f8e52f5520b2d6c654cc3bf1e3ae4bfb45664c9d51b208d0c6 # via phantom-types (pyproject.toml) packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via sphinx phonenumbers==9.0.18 \ --hash=sha256:5537c61ba95b11b992c95e804da6e49193cc06b1224f632ade64631518a48ed1 \ --hash=sha256:d3354454ac31c97f8a08121df97a7145b8dca641f734c6f1518a41c2f60c5764 # via phantom-types (pyproject.toml) pydantic==1.10.24 \ --hash=sha256:02f7a25e8949d8ca568e4bcef2ffed7881d7843286e7c3488bdd3b67f092059c \ --hash=sha256:076fff9da02ca716e4c8299c68512fdfbeac32fdefc9c160e6f80bdadca0993d \ --hash=sha256:093768eba26db55a88b12f3073017e3fdee319ef60d3aef5c6c04a4e484db193 \ --hash=sha256:0cbbf306124ae41cc153fdc2559b37faa1bec9a23ef7b082c1756d1315ceffe6 \ --hash=sha256:17e7610119483f03954569c18d4de16f4e92f1585f20975414033ac2d4a96624 \ --hash=sha256:1a1ae996daa3d43c530b8d0bacc7e2d9cb55e3991f0e6b7cc2cb61a0fb9f6667 \ --hash=sha256:25fb9a69a21d711deb5acefdab9ff8fb49e6cc77fdd46d38217d433bff2e3de2 \ --hash=sha256:265788a1120285c4955f8b3d52b3ea6a52c7a74db097c4c13a4d3567f0c6df3c \ --hash=sha256:2d1a5ef77efeb54def2695f2b8f4301aae8c7aa2b334bd15f61c18ef54317621 \ --hash=sha256:34109b0afa63b36eec2f2b115694e48ae5ee52f7d3c1baa0be36f80e586bda52 \ --hash=sha256:415c638ca5fd57b915a62dd38c18c8e0afe5adf5527be6f8ce16b4636b616816 \ --hash=sha256:49a6f0178063f15eaea6cbcb2dba04db0b73db9834bc7b1e1c4dbea28c7cd22f \ --hash=sha256:4a9e92b9c78d7f3cfa085c21c110e7000894446e24a836d006aabfc6ae3f1813 \ --hash=sha256:4d7336bfcdb8cb58411e6b498772ba2cff84a2ce92f389bae3a8f1bb2c840c49 \ --hash=sha256:50d9f8a207c07f347d4b34806dc576872000d9a60fd481ed9eb78ea8512e0666 \ --hash=sha256:52219b4e70c1db185cfd103a804e416384e1c8950168a2d4f385664c7c35d21a \ --hash=sha256:58d42a7c344882c00e3bb7c6c8c6f62db2e3aafa671f307271c45ad96e8ccf7a \ --hash=sha256:5a42033fac69b9f1f867ecc3a2159f0e94dceb1abfc509ad57e9e88d49774683 \ --hash=sha256:5ce0986799248082e9a5a026c9b5d2f9fa2e24d2afb9b0eace9104334a58fdc1 \ --hash=sha256:5da2775712dda8b89e701ed2a72d5d81d23dbc6af84089da8a0f61a0be439c8c \ --hash=sha256:5fc35569dfd15d3b3fc06a22abee0a45fdde0784be644e650a8769cd0b2abd94 \ --hash=sha256:6af36a8fb3072526b5b38d3f341b12d8f423188e7d185f130c0079fe02cdec7f \ --hash=sha256:6f25d2f792afcd874cc8339c1da1cc52739f4f3d52993ed1f6c263ef2afadc47 \ --hash=sha256:70152291488f8d2bbcf2027b5c28c27724c78a7949c91b466d28ad75d6d12702 \ --hash=sha256:75259be0558ca3af09192ad7b18557f2e9033ad4cbd48c252131f5292f6374fd \ --hash=sha256:7c8bbad6037a87effe9f3739bdf39851add6e0f7e101d103a601c504892ffa70 \ --hash=sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53 \ --hash=sha256:8057172868b0d98f95e6fcddcc5f75d01570e85c6308702dd2c50ea673bc197b \ --hash=sha256:82f951210ebcdb778b1d93075af43adcd04e9ebfd4f44b1baa8eeb21fbd71e36 \ --hash=sha256:874a78e4ed821258295a472e325eee7de3d91ba7a61d0639ce1b0367a3c63d4c \ --hash=sha256:8f2447ca88a7e14fd4d268857521fb37535c53a367b594fa2d7c2551af905993 \ --hash=sha256:956b30638272c51c85caaff76851b60db4b339022c0ee6eca677c41e3646255b \ --hash=sha256:9c377fc30d9ca40dbff5fd79c5a5e1f0d6fff040fa47a18851bb6b0bd040a5d8 \ --hash=sha256:a5bf94042efbc6ab56b18a5921f426ebbeefc04f554a911d76029e7be9057d01 \ --hash=sha256:af31565b12a7db5bfa5fe8c3a4f8fda4d32f5c2929998b1b241f1c22e9ab6e69 \ --hash=sha256:af8e2b3648128b8cadb1a71e2f8092a6f42d4ca123fad7a8d7ce6db8938b1db3 \ --hash=sha256:b644d6f14b2ce617d6def21622f9ba73961a16b7dffdba7f6692e2f66fa05d00 \ --hash=sha256:b66e4892d8ae005f436a5c5f1519ecf837574d8414b1c93860fb3c13943d9b37 \ --hash=sha256:bb3df10be3c7d264947180615819aeec0916f19650f2ba7309ed1fe546ead0d2 \ --hash=sha256:bed9d6eea5fabbc6978c42e947190c7bd628ddaff3b56fc963fe696c3710ccd6 \ --hash=sha256:c626596c1b95dc6d45f7129f10b6743fbb50f29d942d25a22b2ceead670c067d \ --hash=sha256:d255bebd927e5f1e026b32605684f7b6fc36a13e62b07cb97b29027b91657def \ --hash=sha256:d6e45dbc79a44e34c2c83ef1fcb56ff663040474dcf4dfc452db24a1de0f7574 \ --hash=sha256:e24435a9970dcb2b35648f2cf57505d4bd414fcca1a404c82e28d948183fe0a6 \ --hash=sha256:eef07ea2fba12f9188cfa2c50cb3eaa6516b56c33e2a8cc3cd288b4190ee6c0c \ --hash=sha256:ef14dfa7c98b314a3e449e92df6f1479cafe74c626952f353ff0176b075070de \ --hash=sha256:f154a8a46a0d950c055254f8f010ba07e742ac4404a3b6e281a31913ac45ccd0 \ --hash=sha256:fa0ebefc169439267e4b4147c7d458908788367640509ed32c90a91a63ebb579 \ --hash=sha256:fac7fbcb65171959973f3136d0792c3d1668bc01fd414738f0898b01f692f1b4 \ --hash=sha256:fc3f4a6544517380658b63b144c7d43d5276a343012913b7e5d18d9fba2f12bb # via phantom-types (pyproject.toml) pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b # via # accessible-pygments # furo # sphinx python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via phantom-types (pyproject.toml) requests==2.32.5 \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via sphinx roman-numerals-py==3.1.0 \ --hash=sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c \ --hash=sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d # via sphinx six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil snowballstemmer==3.0.1 \ --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 # via sphinx sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via hypothesis soupsieve==2.8 \ --hash=sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c \ --hash=sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f # via beautifulsoup4 sphinx==8.2.3 \ --hash=sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348 \ --hash=sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 # via # phantom-types (pyproject.toml) # furo # sphinx-autodoc-typehints # sphinx-basic-ng sphinx-autodoc-typehints==3.5.2 \ --hash=sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c \ --hash=sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb # via phantom-types (pyproject.toml) sphinx-basic-ng==1.0.0b2 \ --hash=sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9 \ --hash=sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b # via furo sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 # via sphinx sphinxcontrib-devhelp==2.0.0 \ --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 # via sphinx sphinxcontrib-htmlhelp==2.1.0 \ --hash=sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 \ --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 # via sphinx sphinxcontrib-jsmath==1.0.1 \ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 # via sphinx sphinxcontrib-qthelp==2.0.0 \ --hash=sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab \ --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb # via sphinx sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx typeguard==4.4.4 \ --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e # via phantom-types (pyproject.toml) typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # phantom-types (pyproject.toml) # beautifulsoup4 # pydantic # typeguard urllib3==2.5.0 \ --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc # via requests ================================================ FILE: goose.yaml ================================================ environments: - ecosystem: language: python version: "3.13" dependencies: - ruff - pre-commit-hooks - editorconfig-checker - blacken-docs - check-jsonschema - id: check-manifest ecosystem: language: python version: "3.13" dependencies: - check-manifest - setuptools==80.9.0 - setuptools-scm==8.3.1 - wheel==0.45.1 - ecosystem: node dependencies: - prettier hooks: - id: check-manifest environment: check-manifest command: check-manifest parameterize: false read_only: true args: [--no-build-isolation] - id: prettier environment: node command: prettier types: [markdown] args: - --write - --ignore-unknown - --parser=markdown - --print-width=88 - --prose-wrap=always - id: check-case-conflict environment: python command: check-case-conflict read_only: true - id: check-merge-conflict environment: python command: check-merge-conflict read_only: true types: [text] - id: python-debug-statements environment: python command: debug-statement-hook read_only: true types: [python] - id: detect-private-key environment: python command: detect-private-key read_only: true types: [text] - id: end-of-file-fixer environment: python command: end-of-file-fixer types: [text] - id: trailing-whitespace-fixer environment: python command: trailing-whitespace-fixer types: [text] - id: editorconfig-checker environment: python command: ec args: [-disable-indent-size] types: [text] read_only: true - id: ruff-check environment: python command: ruff args: [check, --force-exclude, --fix] types: [python] - id: ruff-format environment: python command: ruff args: [format, --force-exclude] types: [python] - id: blacken-docs environment: python command: blacken-docs types: [markdown, python] - id: check-github-workflows environment: python command: check-jsonschema read_only: true args: ["--builtin-schema", "vendor.github-workflows"] types: [yaml] limit: - "^.github/workflows/" ================================================ FILE: mypy.ini ================================================ [mypy] python_version = 3.10 pretty = True files = src, tests show_error_codes = True show_error_context = True show_error_code_links = True strict = True warn_unreachable = True disallow_any_generics = False [mypy-tests.*] disallow_untyped_defs = False disallow_untyped_calls = False disallow_any_expr = False disallow_untyped_decorators = False disallow_incomplete_defs = False ================================================ FILE: pyproject.toml ================================================ [build-system] requires = [ "setuptools==80.9.0", "setuptools-scm==8.3.1", "wheel==0.45.1", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] version_file = "src/phantom/_version.py" [tool.setuptools] include-package-data = true [tool.setuptools.dynamic] readme = {file = "README.md", content-type = "text/markdown; charset=UTF-8"} [tool.setuptools.packages.find] where = ["src"] namespaces = false [project] name = "phantom-types" description = "Phantom types for Python" requires-python = ">=3.10" authors = [ { name="Anton Agestam", email="git@antonagestam.se" }, ] license = {text = "BSD-3-Clause"} classifiers = [ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Development Status :: 5 - Production/Stable", ] dynamic = ["version", "readme"] dependencies = [ # typeguard 4.3.0 breaks "intersection" protocols, see linked issue. I didn't figure # out a way to work around this at the moment, so it needs to be pinned. # https://github.com/antonagestam/phantom-types/issues/299 "typeguard>=4,!=4.3.*", "typing_extensions>=4.3.0", "numerary>=0.4.3", ] [project.optional-dependencies] phonenumbers = ["phonenumbers>=8.12.41"] pydantic = ["pydantic>=1.9.0,<2"] dateutil = ["python-dateutil>=2.8.2"] hypothesis = ["hypothesis[zoneinfo]>=6.68.0"] all = [ "phantom-types[phonenumbers]", "phantom-types[pydantic]", "phantom-types[dateutil]", "phantom-types[hypothesis]", ] test = [ "mypy>=1.16.1", "pytest", "pytest-mypy-plugins>=1.9.3", "coverage", ] type-check = [ "phantom-types[all]", "mypy", "pytest", "types-python-dateutil", ] docs = [ "phantom-types[all]", "sphinx", "sphinx-autodoc-typehints", "furo", ] [project.urls] "Source Repository" = "https://github.com/antonagestam/phantom-types/" "Documentation" = "https://phantom-types.readthedocs.io/en/stable/" [tool.check-manifest] ignore = ["src/phantom/_version.py"] [tool.pip-tools] generate-hashes = true strip-extras = true upgrade = true unsafe-package = ["phantom-types"] [tool.black] target-version = ["py39"] [tool.pytest.ini_options] testpaths = ["tests", "src", "docs"] addopts = "--mypy-ini-file=mypy.ini --mypy-only-local-stub --doctest-modules --import-mode=importlib" markers = [ "external: mark tests that require extra dependencies", "no_external: mark tests that will fail if run with extra dependencies", ] ================================================ FILE: ruff.toml ================================================ fix = true target-version = "py310" [lint] extend-select = [ # bugbear "B", # comprehensions "C4", # mccabe "C90", # bandit "S", # blind exception # Bare excepts are caught without this, but this also catches `except Exception: ...`. "BLE", # builtins "A", # Enforce valid noqa comments. "RUF100", # isort "I", # pycodestyle "W", # pyupgrade "UP", # debugger "T10", # print "T20", # quotes "Q", # return # This gives 3 false positives, would be nice otherwise probably. # "RET", # simplify "SIM", # tidy imports # We use this to only outlaw relative parent imports. "TID", ] extend-ignore = [ # There's no reason to outlaw asserts. "S101", # False positives. "A005", ] isort.force-single-line = true isort.known-first-party = ["phantom", "tests"] flake8-tidy-imports.ban-relative-imports = "parents" mccabe.max-complexity = 10 ================================================ FILE: setup.cfg ================================================ [coverage:run] source = phantom branch = True [coverage:report] skip_covered = True show_missing = True exclude_lines = pragma: no cover # ignore non-implementations ^\s*\.\.\. ^\s*if TYPE_CHECKING: ^\s*def [\w_]+\(.*?\) -> [\w_]+: ...$ ================================================ FILE: src/phantom/__init__.py ================================================ """ Use ``Phantom`` to create arbitrary phantom types using boolean predicates. .. code-block:: python from phantom import Phantom def is_big(value: int) -> bool: return value > 5 class Big(int, Phantom, predicate=is_big): ... assert isinstance(10, Big) # this passes """ from ._base import Phantom from ._base import PhantomBase from ._base import PhantomMeta from ._version import __version__ from ._version import __version_tuple__ from .bounds import get_bound_parser from .errors import BoundError from .predicates import Predicate __all__ = ( "__version__", "__version_tuple__", "BoundError", "Phantom", "PhantomBase", "PhantomMeta", "get_bound_parser", "Predicate", ) ================================================ FILE: src/phantom/_base.py ================================================ from __future__ import annotations import abc from collections.abc import Callable from collections.abc import Iterable from collections.abc import Iterator from typing import Any from typing import ClassVar from typing import Generic from typing import Protocol from typing import TypeVar from typing import runtime_checkable from typing_extensions import Self from . import _hypothesis from ._utils.misc import BoundType from ._utils.misc import UnresolvedClassAttribute from ._utils.misc import fully_qualified_name from ._utils.misc import is_not_known_mutable_type from ._utils.misc import is_subtype from ._utils.misc import resolve_class_attr from .bounds import Parser from .bounds import get_bound_parser from .errors import BoundError from .predicates import Predicate from .schema import SchemaField @runtime_checkable class InstanceCheckable(Protocol): @classmethod @abc.abstractmethod def __instancecheck__(cls, instance: object) -> bool: ... class SupportsParse(Protocol): @classmethod @abc.abstractmethod def parse(cls, instance: object) -> Self: ... V = TypeVar("V", bound=SupportsParse) class PhantomMeta(abc.ABCMeta): """ Metaclass that defers __instancecheck__ to derived classes and prevents actual instance creation. """ def __instancecheck__(self, instance: object) -> bool: if not issubclass(self, InstanceCheckable): return False return self.__instancecheck__(instance) def __call__(cls: type[V], instance: object) -> V: return cls.parse(instance) T = TypeVar("T", covariant=True) U = TypeVar("U") Derived = TypeVar("Derived", bound="PhantomBase") class PhantomBase(SchemaField, metaclass=PhantomMeta): @classmethod def parse(cls: type[Derived], instance: object) -> Derived: """ Parse an arbitrary value into a phantom type. :raises TypeError: """ if not isinstance(instance, cls): raise TypeError( f"Could not parse {fully_qualified_name(cls)} from {instance!r}" ) return instance @classmethod @abc.abstractmethod def __instancecheck__(cls, instance: object) -> bool: ... @classmethod def __get_validators__(cls: type[Derived]) -> Iterator[Callable[[object], Derived]]: """Hook that makes phantom types compatible with pydantic.""" yield cls.parse class AbstractInstanceCheck(TypeError): ... class MutableType(TypeError): ... class Phantom(PhantomBase, Generic[T]): """ Base class for predicate-based phantom types. **Class arguments** * ``predicate: Predicate[T] | None`` - Predicate function used for instance checks. Can be ``None`` if the type is abstract. * ``bound: type[T] | None`` - Bound used to check values before passing them to the type's predicate function. This will often but not always be the same as the runtime type that values of the phantom type are represented as. If this is not provided as a class argument, it's attempted to be resolved in order from an implicit bound (any bases of the type that come before ``Phantom``), or inherited from super phantom types that provide a bound. Can be ``None`` if the type is abstract. * ``abstract: bool`` - Set to ``True`` to create an abstract phantom type. This allows deferring definitions of ``predicate`` and ``bound`` to concrete subtypes. """ __predicate__: Predicate[T] # The bound of a phantom type is the type that its values will have at # runtime, so when checking if a value is an instance of a phantom type, # it's first checked to be within its bounds, so that the value can be # safely passed as argument to the predicate function. # # When subclassing, the bound of the new type must be a subtype of the bound # of the super class. __bound__: ClassVar[type] __abstract__: ClassVar[bool] def __init_subclass__( cls, predicate: Predicate[T] | None = None, bound: type[T] | None = None, abstract: bool = False, **kwargs: Any, ) -> None: super().__init_subclass__(**kwargs) resolve_class_attr(cls, "__abstract__", abstract) resolve_class_attr(cls, "__predicate__", predicate) cls._resolve_bound(bound) if _hypothesis.register_type_strategy is not None and not cls.__abstract__: strategy = cls.__register_strategy__() if strategy is not None: _hypothesis.register_type_strategy(cls, strategy) @classmethod def _interpret_implicit_bound(cls) -> BoundType: def discover_bounds() -> Iterable[type]: for type_ in cls.__mro__: if type_ is cls: continue if issubclass(type_, Phantom): break yield type_ else: # pragma: no cover raise RuntimeError(f"{cls} is not a subclass of Phantom") types = tuple(discover_bounds()) if len(types) == 1: return types[0] return types @classmethod def _resolve_bound(cls, class_arg: Any) -> None: inherited = getattr(cls, "__bound__", None) implicit = cls._interpret_implicit_bound() if class_arg is not None: bound = class_arg elif implicit: bound = implicit elif inherited is not None: bound = inherited elif not getattr(cls, "__abstract__", False): raise UnresolvedClassAttribute( f"Concrete phantom type {cls.__qualname__} must define class attribute " f"__bound__." ) else: return if inherited is not None and not is_subtype(bound, inherited): raise BoundError( f"The bound of {cls.__qualname__} is not compatible with its " f"inherited bounds." ) if not is_not_known_mutable_type(bound): raise MutableType(f"The bound of {cls.__qualname__} is mutable.") cls.__bound__ = bound @classmethod def __instancecheck__(cls, instance: object) -> bool: if cls.__abstract__: raise AbstractInstanceCheck( "Abstract phantom types cannot be used in instance checks" ) bound_parser: Parser[T] = get_bound_parser(cls.__bound__) try: instance = bound_parser(instance) except BoundError: return False return cls.__predicate__(instance) @classmethod def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy | None: return None ================================================ FILE: src/phantom/_hypothesis.py ================================================ from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING from typing import TypeAlias from typing import TypeVar if TYPE_CHECKING: from hypothesis.strategies import SearchStrategy else: SearchStrategy = None __all__ = ("HypothesisStrategy", "register_type_strategy", "SearchStrategy") T = TypeVar("T") HypothesisStrategy: TypeAlias = ( "SearchStrategy | Callable[[type[T]], SearchStrategy[T] | None]" ) register_type_strategy: Callable[[type, HypothesisStrategy], None] | None try: from hypothesis.strategies import register_type_strategy # type: ignore[assignment] except ImportError: register_type_strategy = None ================================================ FILE: src/phantom/_utils/__init__.py ================================================ ================================================ FILE: src/phantom/_utils/misc.py ================================================ # Workaround for something that looks like this bug # https://github.com/pytest-dev/pytest/issues/4386 from __future__ import annotations from collections.abc import MutableMapping from collections.abc import MutableSequence from collections.abc import MutableSet from dataclasses import is_dataclass from itertools import product from types import UnionType from typing import Final from typing import NewType from typing import TypeAlias from typing import TypeGuard from typing import Union from typing import get_args from typing import get_origin class UnresolvedClassAttribute(NotImplementedError): ... def resolve_class_attr( cls: type, name: str, argument: object | None, required: bool = True, ) -> None: argument = getattr(cls, name, None) if argument is None else argument if argument is not None: setattr(cls, name, argument) elif required and not getattr(cls, "__abstract__", False): raise UnresolvedClassAttribute( f"Concrete phantom type {cls.__qualname__} must define class attribute " f"{name}." ) BoundType: TypeAlias = type | tuple[type, ...] def _is_union(type_: BoundType) -> bool: return get_origin(type_) in (Union, UnionType) def _is_intersection(type_: BoundType) -> bool: return isinstance(type_, tuple) def is_subtype(a: BoundType, b: BoundType) -> bool: # noqa: C901 """ Return True if ``a`` is a subtype of ``b``. Supports single-level typing.Unions and intersections represented as tuples respectively without nesting. The cases that this function deals with can be divided into the following cases, where T is neither a union or intersection: 1. Union, Union: Success if all types in a have a subclass in b. 2. T, Union: Success if a is a subclass of one or more types in b. 3. Union, T: Always fails (except when a single-type union, see 9). 4. T, Intersection: Success if a is a subclass of all types in b. 5. Intersection, T: Success if one type in a is a subclass of b. 6. Intersection, Union: Success if one type in a is a subclass of one type in b. 7. Union, Intersection: Always fails (except when a is a single-type union see 4). 8. Intersection, Intersection: Success if all items in b have a subclass in a. 9. T, T: Success if a is a subclass of b. """ if _is_union(a) and _is_union(b): for a_part in get_args(a): for b_part in get_args(b): if issubclass(a_part, b_part): break else: return False return True elif _is_intersection(a) and _is_union(b): assert isinstance(a, tuple) for a_part, b_part in product(a, get_args(b)): if issubclass(a_part, b_part): return True return False elif _is_intersection(a) and _is_intersection(b): assert isinstance(a, tuple) assert isinstance(b, tuple) for b_part in b: for a_part in a: if issubclass(a_part, b_part): break else: return False return True elif _is_union(a): return False elif _is_union(b): assert isinstance(a, type) return any(issubclass(a, b_part) for b_part in get_args(b)) elif _is_intersection(b): assert isinstance(a, type) assert isinstance(b, tuple) return all(issubclass(a, b_part) for b_part in b) elif _is_intersection(a): assert isinstance(a, tuple) return any(issubclass(a_part, b) for a_part in a) assert isinstance(a, type) return issubclass(a, b) def fully_qualified_name(cls: type) -> str: return f"{cls.__module__}.{cls.__qualname__}" mutable: Final = (MutableSequence, MutableSet, MutableMapping) NotKnownMutableType = NewType("NotKnownMutableType", type) """ Internal type to mark types that are not known to be mutable. The term immutable is avoided here because there is no way to guarantee that a checked type isn't actually mutable, so we don't want to communicate in strong terms here. """ def is_not_known_mutable_type(type_: BoundType) -> TypeGuard[NotKnownMutableType]: return not ( any(is_subtype(type_, mutable_type) for mutable_type in mutable) or ( is_dataclass(type_) and not type_.__dataclass_params__.frozen # type: ignore[attr-defined] ) ) def is_not_known_mutable_instance(value: object) -> bool: return not ( isinstance(value, mutable) or ( is_dataclass(value) # https://github.com/python/typeshed/pull/9947#issue-1641078469 and not value.__dataclass_params__.frozen # type: ignore[union-attr] ) ) def is_union(value: object) -> TypeGuard[type]: return get_origin(value) == Union or isinstance(value, UnionType) ================================================ FILE: src/phantom/_utils/types.py ================================================ from typing import Protocol from typing import TypeVar from typing import runtime_checkable from numerary.protocol import CachingProtocolMeta T_contra = TypeVar("T_contra", contravariant=True) U_co = TypeVar("U_co", covariant=True) @runtime_checkable class _SupportsLt(Protocol[T_contra]): def __lt__(self, other: T_contra) -> bool: ... class SupportsLt( _SupportsLt[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsLe(Protocol[T_contra]): def __le__(self, other: T_contra) -> bool: ... class SupportsLe( _SupportsLe[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsGt(Protocol[T_contra]): def __gt__(self, other: T_contra) -> bool: ... class SupportsGt( _SupportsGt[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsGe(Protocol[T_contra]): def __ge__(self, other: T_contra) -> bool: ... class SupportsGe( _SupportsGe[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsEq(Protocol): def __eq__(self, other: object) -> bool: ... def __hash__(self) -> int: ... class SupportsEq( _SupportsEq, Protocol, metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _Comparable( SupportsLt[T_contra], SupportsLe[T_contra], SupportsGt[T_contra], SupportsGe[T_contra], SupportsEq, Protocol[T_contra], ): ... class Comparable( _Comparable[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsFloat(Protocol): def __float__(self) -> float: ... class SupportsFloat(_SupportsFloat, Protocol, metaclass=CachingProtocolMeta): ... @runtime_checkable class _FloatComparable( SupportsFloat, Comparable[T_contra], Protocol[T_contra], ): ... class FloatComparable( _FloatComparable[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsLeGe(SupportsLe[T_contra], SupportsGe[T_contra], Protocol[T_contra]): ... class SupportsLeGe( _SupportsLeGe[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsLeGt(SupportsLe[T_contra], SupportsGt[T_contra], Protocol[T_contra]): ... class SupportsLeGt( _SupportsLeGt[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsLtGe(SupportsLt[T_contra], SupportsGe[T_contra], Protocol[T_contra]): ... class SupportsLtGe( _SupportsLtGe[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... class _SupportsLtGt(SupportsLt[T_contra], SupportsGt[T_contra], Protocol[T_contra]): ... class SupportsLtGt( _SupportsLtGt[T_contra], Protocol[T_contra], metaclass=CachingProtocolMeta, ): ... @runtime_checkable class _SupportsMod(Protocol[T_contra, U_co]): def __mod__(self, other: T_contra) -> U_co: ... class SupportsMod( _SupportsMod[T_contra, U_co], Protocol[T_contra, U_co], metaclass=CachingProtocolMeta, ): ... ================================================ FILE: src/phantom/boolean.py ================================================ """ Types describing objects that coerce to either ``True`` or ``False`` respectively when calling ``bool()`` on them. """ from __future__ import annotations from typing import TYPE_CHECKING from . import Phantom from .predicates import boolean if TYPE_CHECKING: from hypothesis.strategies import SearchStrategy class Truthy(Phantom[object], predicate=boolean.truthy, bound=object): """ >>> isinstance("Huzzah!", Truthy) True >>> isinstance((), Truthy) False """ @classmethod def __register_strategy__(cls) -> SearchStrategy: from hypothesis.strategies import integers from hypothesis.strategies import just from hypothesis.strategies import lists from hypothesis.strategies import text return ( just(True) | integers(min_value=1) | integers(max_value=-1) | lists(elements=integers(), min_size=1) | text(min_size=1) ) class Falsy(Phantom[object], predicate=boolean.falsy, bound=object): """ >>> isinstance((), Falsy) True >>> isinstance("Hej!", Falsy) False """ @classmethod def __register_strategy__(cls) -> SearchStrategy: from hypothesis.strategies import just return just(False) | just(0) | just(()) | just("") | just(b"") ================================================ FILE: src/phantom/bounds.py ================================================ from __future__ import annotations from collections.abc import Callable from collections.abc import Iterable from collections.abc import Sequence from typing import Any from typing import Final from typing import TypeAlias from typing import TypeVar from typing import cast from typing import get_args from ._utils.misc import is_union from .errors import BoundError from .predicates.boolean import all_of from .predicates.generic import of_complex_type from .predicates.generic import of_type __all__ = ("get_bound_parser", "parse_str", "Parser") T = TypeVar("T", covariant=True) Parser: TypeAlias = Callable[[object], T] def display_bound(bound: Any) -> str: if isinstance(bound, Iterable): return f"Intersection[{', '.join(display_bound(part) for part in bound)}]" if is_union(bound): return ( f"typing.Union[" f"{', '.join(display_bound(part) for part in get_args(bound))}" f"]" ) return str(getattr(bound, "__name__", bound)) def get_bound_parser(bound: type[T] | Any) -> Parser[T]: within_bound = ( # Interpret sequence as intersection all_of(of_type(t) for t in bound) if isinstance(bound, Sequence) else of_complex_type(bound) ) def parser(instance: object) -> T: if not within_bound(instance): raise BoundError( f"Value is not within bound of {display_bound(bound)!r}: {instance!r}" ) return cast(T, instance) return parser parse_str: Final[Callable[[object], str]] = get_bound_parser(str) ================================================ FILE: src/phantom/datetime.py ================================================ """ Types for narrowing on the builtin datetime types. These types can be used without installing any extra dependencies, however, to parse strings, python-dateutil must be installed or a :py:class:`phantom.errors.MissingDependency` error will be raised when calling parse. You can install python-dateutil by using the ``[dateutil]`` or ``[all]`` extras. """ from __future__ import annotations import datetime from . import Phantom from . import _hypothesis from .bounds import parse_str from .errors import MissingDependency from .predicates.datetime import is_tz_aware from .predicates.datetime import is_tz_naive from .schema import Schema try: import dateutil.parser parse_datetime_str = dateutil.parser.parse DateutilParseError = dateutil.parser.ParserError except ImportError as e: exception = e def parse_datetime_str( *_: object, **__: object, ) -> datetime.datetime: raise MissingDependency( "python-dateutil needs to be installed to use this type for parsing. It " "can be installed with the phantom-types[dateutil] extra." ) from exception class DateutilParseError(Exception): # type: ignore[no-redef] ... __all__ = ("TZAware", "TZNaive") def parse_datetime(value: object) -> datetime.datetime: if isinstance(value, datetime.datetime): return value str_value = parse_str(value) try: return parse_datetime_str(str_value) except DateutilParseError as exc: raise TypeError("Could not parse datetime from given string") from exc class TZAware(datetime.datetime, Phantom, predicate=is_tz_aware): """ A type for helping ensure that ``datetime`` objects are always timezone aware. >>> isinstance(datetime.datetime.now(), TZAware) False >>> isinstance(datetime.datetime.now(tz=datetime.timezone.utc), TZAware) True """ # A property of being aware is (dt.tzinfo != None), so we can safely narrow this # attribute to not include None. tzinfo: datetime.tzinfo @classmethod def parse(cls, instance: object) -> TZAware: return super().parse(parse_datetime(instance)) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A date-time with timezone data.", } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy: from hypothesis.strategies import datetimes from hypothesis.strategies import timezones return datetimes(timezones=timezones()) class TZNaive(datetime.datetime, Phantom, predicate=is_tz_naive): """ >>> isinstance(datetime.datetime.now(), TZNaive) True >>> isinstance(datetime.datetime.now(tz=datetime.timezone.utc), TZNaive) False """ @classmethod def parse(cls, instance: object) -> TZNaive: return super().parse(parse_datetime(instance)) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A date-time without timezone data.", "format": "date-time-naive", } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy: from hypothesis.strategies import datetimes from hypothesis.strategies import none return datetimes(timezones=none()) ================================================ FILE: src/phantom/errors.py ================================================ class BoundError(TypeError): ... class MissingDependency(Exception): ... ================================================ FILE: src/phantom/ext/__init__.py ================================================ ================================================ FILE: src/phantom/ext/phonenumbers.py ================================================ """ Requires the phonenumbers_ package which can be installed with: .. _phonenumbers: https://pypi.org/project/phonenumbers/ .. code-block:: bash $ python3 -m pip install phantom-types[phonenumbers] """ from __future__ import annotations from typing import Final from typing import TypeGuard from typing import cast import phonenumbers from phantom import Phantom from phantom.bounds import parse_str from phantom.fn import excepts from phantom.schema import Schema __all__ = ( "InvalidPhoneNumber", "normalize_phone_number", "is_phone_number", "is_formatted_phone_number", "PhoneNumber", "FormattedPhoneNumber", ) class InvalidPhoneNumber(phonenumbers.NumberParseException, TypeError): INVALID: Final = 99 def __init__(self, error_type: int = INVALID, msg: str = "Invalid number") -> None: super().__init__(error_type, msg) def _deconstruct_phone_number( phone_number: str, country_code: str | None = None ) -> phonenumbers.PhoneNumber: try: parsed_number = phonenumbers.parse(phone_number, region=country_code) except phonenumbers.NumberParseException as e: raise InvalidPhoneNumber(e.error_type, e._msg) from e if not phonenumbers.is_valid_number(parsed_number): raise InvalidPhoneNumber return parsed_number def normalize_phone_number( phone_number: str, country_code: str | None = None, ) -> FormattedPhoneNumber: """ Normalize ``phone_number`` using :py:const:`phonenumbers.PhoneNumberFormat.E164`. :raises InvalidPhoneNumber: """ normalized = phonenumbers.format_number( _deconstruct_phone_number(phone_number, country_code), phonenumbers.PhoneNumberFormat.E164, ) return cast(FormattedPhoneNumber, normalized) is_phone_number = excepts(InvalidPhoneNumber)(_deconstruct_phone_number) def is_formatted_phone_number(number: str) -> TypeGuard[FormattedPhoneNumber]: try: return number == normalize_phone_number(number) except InvalidPhoneNumber: return False class PhoneNumber(str, Phantom, predicate=is_phone_number): @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A valid E.164 phone number.", "type": "string", "format": "E.164", } class FormattedPhoneNumber(PhoneNumber, predicate=is_formatted_phone_number): @classmethod def parse(cls, instance: object) -> FormattedPhoneNumber: """ Normalize number using :py:const:`phonenumbers.PhoneNumberFormat.E164`. :raises InvalidPhoneNumber: """ return normalize_phone_number(parse_str(instance)) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "title": "PhoneNumber", } ================================================ FILE: src/phantom/fn.py ================================================ from __future__ import annotations import functools from collections.abc import Callable from functools import partial from typing import Any from typing import TypeVar def _name(fn: Callable) -> str: if isinstance(fn, partial): fn = fn.func try: return fn.__qualname__ except AttributeError: return str(fn) AA = TypeVar("AA") AR = TypeVar("AR") BA = TypeVar("BA") def compose2(a: Callable[[AA], AR], b: Callable[[BA], AA]) -> Callable[[BA], AR]: """ Returns a function composed from the two given functions ``a`` and ``b`` such that calling ``compose2(a, b)(x)`` is equivalent to calling ``a(b(x))``. >>> compose2("".join, reversed)("!olleH") 'Hello!' """ a_name = _name(a) b_name = _name(b) def c(arg: BA) -> AR: return a(b(arg)) c.__name__ = f"{a_name}∘{b_name}" c.__doc__ = f"Function composed as {a_name}({b_name}(_))." return c A = TypeVar("A") def excepts( exception: tuple[type[Exception], ...] | type[Exception], negate: bool = False, ) -> Callable[[Callable[[A], Any]], Callable[[A], bool]]: """ Turn a unary function that raises an exception into a boolean predicate. >>> def validate_positive(number: int) -> None: ... if number < 0: raise ValueError >>> is_positive = excepts(ValueError)(validate_positive) >>> is_positive(0), is_positive(-1) (True, False) """ def decorator(fn: Callable[[A], Any]) -> Callable[[A], bool]: @functools.wraps(fn) def wrapper(arg: A) -> bool: try: fn(arg) except exception: return negate return not negate return wrapper return decorator ================================================ FILE: src/phantom/interval.py ================================================ """ Types for describing narrower sets of numbers than builtin numeric types like ``int`` and ``float``. Use the provided base classes to build custom intervals. For example, to represent number in the closed range ``[0, 100]`` for a volume control you would define a type like this: .. code-block:: python class VolumeLevel(int, Inclusive, low=0, high=100): ... There is also a set of concrete ready-to-use interval types provided, that use predicate functions from :py:mod:`phantom.predicates.interval`. .. code-block:: python def take_portion(portion: Portion, whole: Natural) -> float: return portion * whole All interval types fully support pydantic and appropriately adds inclusive or exclusive minimums and maximums to their schema representations. """ from __future__ import annotations from contextlib import suppress from typing import Any from typing import Final from typing import Protocol from typing import TypeVar from . import Phantom from . import Predicate from . import _hypothesis from ._utils.misc import resolve_class_attr from ._utils.types import Comparable from ._utils.types import FloatComparable from ._utils.types import SupportsEq from .predicates import interval from .schema import Schema N = TypeVar("N", bound=Comparable) Derived = TypeVar("Derived", bound="Interval") class IntervalCheck(Protocol): def __call__(self, a: N, b: N) -> Predicate[N]: ... inf: Final = float("inf") neg_inf: Final = float("-inf") class _NonScalarBounds(Exception): ... def _get_scalar_int_bounds( type_: type[Interval], exclude_min: bool = False, exclude_max: bool = False, ) -> tuple[int | None, int | None]: low = type_.__low__ if type_.__low__ != neg_inf else None high = type_.__high__ if type_.__high__ != inf else None if low is not None: try: scalar_low = int(low) # type: ignore[call-overload] except TypeError as exception: raise _NonScalarBounds from exception if exclude_min: scalar_low += 1 else: scalar_low = None if high is not None: try: scalar_high = int(high) # type: ignore[call-overload] except TypeError as exception: raise _NonScalarBounds from exception if exclude_max: scalar_high -= 1 else: scalar_high = None return scalar_low, scalar_high def _get_scalar_float_bounds( type_: type[Interval], ) -> tuple[float | None, float | None]: low = type_.__low__ if type_.__low__ != neg_inf else None high = type_.__high__ if type_.__high__ != inf else None if low is not None: try: low = float(low) except TypeError as excpetion: raise _NonScalarBounds from excpetion if high is not None: try: high = float(high) except TypeError as exception: raise _NonScalarBounds from exception return low, high def _resolve_bound( cls: type, name: str, argument: Comparable | None, default: Comparable, ) -> None: inherited = getattr(cls, name, None) if argument is not None: resolved = argument elif inherited is not None: resolved = inherited else: resolved = default setattr(cls, name, resolved) class Interval(Phantom[Comparable], bound=Comparable, abstract=True): """ Base class for all interval types, providing the following class arguments: * ``check: IntervalCheck`` * ``low: Comparable`` (defaults to negative infinity) * ``high: Comparable`` (defaults to positive infinity) Concrete subclasses must specify their runtime type bound as their first base. """ __check__: IntervalCheck __low__: FloatComparable __high__: FloatComparable def __init_subclass__( cls, check: IntervalCheck | None = None, low: FloatComparable | None = None, high: FloatComparable | None = None, **kwargs: Any, ) -> None: _resolve_bound(cls, "__low__", low, neg_inf) _resolve_bound(cls, "__high__", high, inf) resolve_class_attr(cls, "__check__", check) if getattr(cls, "__check__", None) is None: raise TypeError(f"{cls.__qualname__} must define an interval check") super().__init_subclass__( predicate=cls.__check__(cls.__low__, cls.__high__), **kwargs, ) @classmethod def parse(cls: type[Derived], instance: object) -> Derived: return super().parse( cls.__bound__(instance) if isinstance(instance, str) else instance ) def _format_limit(value: SupportsEq) -> str: if value == inf: return "∞" if value == neg_inf: return "-∞" return str(value) class Exclusive(Interval, check=interval.exclusive, abstract=True): """Uses :py:func:`phantom.predicates.interval.exclusive` as ``check``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": ( f"A value in the exclusive range ({_format_limit(cls.__low__)}, " f"{_format_limit(cls.__high__)})." ), "exclusiveMinimum": float(cls.__low__) if cls.__low__ != neg_inf else None, "exclusiveMaximum": float(cls.__high__) if cls.__high__ != inf else None, } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import floats from hypothesis.strategies import integers with suppress(_NonScalarBounds): # pragma: no cover if issubclass(cls.__bound__, int): return integers( *_get_scalar_int_bounds(cls, exclude_min=True, exclude_max=True) ) if issubclass(cls.__bound__, float): return floats( *_get_scalar_float_bounds(cls), exclude_min=True, exclude_max=True ) return None class Inclusive(Interval, check=interval.inclusive, abstract=True): """Uses :py:func:`phantom.predicates.interval.inclusive` as ``check``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": ( f"A value in the inclusive range [{_format_limit(cls.__low__)}, " f"{_format_limit(cls.__high__)}]." ), "minimum": float(cls.__low__) if cls.__low__ != neg_inf else None, "maximum": float(cls.__high__) if cls.__high__ != inf else None, } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import floats from hypothesis.strategies import integers with suppress(_NonScalarBounds): # pragma: no cover if issubclass(cls.__bound__, int): return integers(*_get_scalar_int_bounds(cls)) if issubclass(cls.__bound__, float): return floats(*_get_scalar_float_bounds(cls)) return None class ExclusiveInclusive(Interval, check=interval.exclusive_inclusive, abstract=True): """Uses :py:func:`phantom.predicates.interval.exclusive_inclusive` as ``check``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": ( f"A value in the half-open range ({_format_limit(cls.__low__)}, " f"{_format_limit(cls.__high__)}]." ), "exclusiveMinimum": float(cls.__low__) if cls.__low__ != neg_inf else None, "maximum": float(cls.__high__) if cls.__high__ != inf else None, } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import floats from hypothesis.strategies import integers with suppress(_NonScalarBounds): # pragma: no cover if issubclass(cls.__bound__, int): return integers(*_get_scalar_int_bounds(cls, exclude_min=True)) if issubclass(cls.__bound__, float): return floats(*_get_scalar_float_bounds(cls), exclude_min=True) return None class InclusiveExclusive(Interval, check=interval.inclusive_exclusive, abstract=True): """Uses :py:func:`phantom.predicates.interval.inclusive_exclusive` as ``check``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": ( f"A value in the half-open range [{_format_limit(cls.__low__)}, " f"{_format_limit(cls.__high__)})." ), "minimum": float(cls.__low__) if cls.__low__ != neg_inf else None, "exclusiveMaximum": float(cls.__high__) if cls.__high__ != inf else None, } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import floats from hypothesis.strategies import integers with suppress(_NonScalarBounds): # pragma: no cover if issubclass(cls.__bound__, int): return integers(*_get_scalar_int_bounds(cls, exclude_max=True)) if issubclass(cls.__bound__, float): return floats(*_get_scalar_float_bounds(cls), exclude_max=True) return None class Natural(int, InclusiveExclusive, low=0): """Represents integer values in the inclusive range ``[0, ∞)``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "An integer value in the inclusive range [0, ∞).", } class NegativeInt(int, ExclusiveInclusive, high=0): """Represents integer values in the inclusive range ``(-∞, 0]``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "An integer value in the inclusive range (-∞, 0].", } class Portion(float, Inclusive, low=0, high=1): """Represents float values in the inclusive range ``[0, 1]``.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A float value in the inclusive range [0, 1].", } ================================================ FILE: src/phantom/iso3166.py ================================================ """ Exposes a :py:class:`CountryCode` type that is a union of a :py:class:`Literal` containing all ISO3166 alpha-2 codes, and a phantom type that parses alpha-2 codes at runtime. This allows mixing statically known values with runtime-parsed values, like so: .. code-block:: python countries: tuple[CountryCode] = ("SE", "DK", ParsedAlpha2.parse("FR")) """ from __future__ import annotations from typing import Final from typing import Literal from typing import TypeAlias from typing import cast from typing import get_args from phantom import Phantom from phantom import _hypothesis from phantom.bounds import parse_str from phantom.predicates.collection import contained from phantom.schema import Schema __all__ = ( "LiteralAlpha2", "ParsedAlpha2", "Alpha2", "CountryCode", "is_alpha2_country_code", "normalize_alpha2_country_code", "InvalidCountryCode", ) LiteralAlpha2: TypeAlias = Literal[ "AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW", "BV", "BR", "IO", "BN", "BG", "BF", "BI", "KH", "CM", "CA", "CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CG", "CD", "CK", "CR", "CI", "HR", "CU", "CW", "CY", "CZ", "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL", "IT", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KP", "KR", "XK", "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NC", "NZ", "NI", "NE", "NG", "NU", "NF", "MP", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SX", "SK", "SI", "SB", "SO", "ZA", "GS", "SS", "ES", "LK", "SD", "SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM", "UY", "UZ", "VU", "VE", "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW", ] """Literal of all ISO3166 alpha-2 codes. """ ALPHA2: Final = frozenset(get_args(LiteralAlpha2)) is_alpha2_country_code = contained(ALPHA2) class InvalidCountryCode(TypeError): ... def normalize_alpha2_country_code(country_code: str) -> ParsedAlpha2: """ Normalize mixed case country code. :raises InvalidCountryCode: """ normalized = country_code.upper() if not is_alpha2_country_code(normalized): raise InvalidCountryCode return cast(ParsedAlpha2, normalized) class ParsedAlpha2(str, Phantom, predicate=is_alpha2_country_code): @classmethod def parse(cls, instance: object) -> ParsedAlpha2: """ Normalize mixed case country code. :raises InvalidCountryCode: """ return normalize_alpha2_country_code(parse_str(instance)) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "ISO3166-1 alpha-2 country code", "examples": ["NR", "KZ", "ET", "VC", "AE", "NZ", "SX", "XK", "AX"], "format": "iso3166-1 alpha-2", "title": "Alpha2", } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import sampled_from return sampled_from(sorted(ALPHA2)) Alpha2: TypeAlias = LiteralAlpha2 | ParsedAlpha2 CountryCode: TypeAlias = Alpha2 ================================================ FILE: src/phantom/negated.py ================================================ """ This module provides a single type: :py:class:`SequenceNotStr`. This type is equivalent to :py:class:`typing.Sequence` except it excludes values of type :py:class:`str` and :py:class:`bytes` from the set of valid instances. This can be useful when you want to eliminate the easy mistake of forgetting to wrap a string value in a containing sequence. """ from __future__ import annotations from collections.abc import Sequence from typing import Generic from typing import TypeVar from typing import get_args from . import Phantom from . import _hypothesis from .predicates import boolean from .predicates.generic import of_type __all__ = ("SequenceNotStr",) T = TypeVar("T") class SequenceNotStr( Sequence[T], Phantom, Generic[T], # Note: We don't eliminate mutable types here like in PhantomSized. This is because # the property of not being a str cannot change by mutation, so this specific # phantom type is safe to use with mutable types. predicate=boolean.negate(of_type((str, bytes))), ): @classmethod def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy: from hypothesis.strategies import from_type from hypothesis.strategies import tuples def create_strategy( type_: type[T], ) -> _hypothesis.SearchStrategy[tuple[T, ...]] | None: (inner_type,) = get_args(type_) return tuples(from_type(inner_type)) return create_strategy ================================================ FILE: src/phantom/predicates/__init__.py ================================================ from ._base import Predicate __all__ = ("Predicate",) ================================================ FILE: src/phantom/predicates/_base.py ================================================ from collections.abc import Callable from typing import TypeAlias from typing import TypeVar T_contra = TypeVar("T_contra", bound=object, contravariant=True) Predicate: TypeAlias = Callable[[T_contra], bool] ================================================ FILE: src/phantom/predicates/_utils.py ================================================ from collections.abc import Callable from functools import partial from typing import TypeVar def _explode_partial(obj: partial) -> str: positional_args = ", ".join(map(repr, obj.args)) keyword_args = ", ".join( f"{name}={value!r}" for name, value in obj.keywords.items() ) args = ", ".join((positional_args, keyword_args)) return f"{obj.func.__qualname__}({args})" def _name_or_repr(obj: object) -> str: if isinstance(obj, partial): return _explode_partial(obj) try: return str(obj.__qualname__) # type: ignore[attr-defined] except AttributeError: return repr(obj) B = TypeVar("B", bound=Callable) def bind_name(wrapped: Callable, *values: object) -> Callable[[B], B]: name = ( f"{wrapped.__qualname__}({', '.join(_name_or_repr(value) for value in values)})" ) def decorator(inner: B) -> B: inner.__qualname__ = inner.__name__ = name return inner return decorator ================================================ FILE: src/phantom/predicates/boolean.py ================================================ from collections.abc import Iterable from typing import Literal from typing import TypeVar from . import Predicate from ._utils import bind_name T_contra = TypeVar("T_contra", bound=object, contravariant=True) def true(_value: object) -> Literal[True]: """Always return :py:const:`True`.""" return True def false(_value: object) -> Literal[False]: """Always return :py:const:`False`.""" return False def negate(predicate: Predicate[T_contra]) -> Predicate[T_contra]: """Negate a given predicate.""" @bind_name(negate, predicate) def check(value: T_contra) -> bool: return not predicate(value) return check def truthy(value: object) -> bool: """Return :py:const:`True` for truthy objects.""" return bool(value) def falsy(value: object) -> bool: """Return :py:const:`True` for falsy objects.""" return negate(truthy)(value) def both(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[T_contra]: """ Create a new predicate that succeeds when both of the given predicates succeed. """ @bind_name(both, p, q) def check(value: T_contra) -> bool: return p(value) and q(value) return check def either(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[T_contra]: """ Create a new predicate that succeeds when at least one of the given predicates succeed. """ @bind_name(either, p, q) def check(value: T_contra) -> bool: return p(value) or q(value) return check def xor(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[T_contra]: """ Create a new predicate that succeeds when one of the given predicates succeed, but not both. """ @bind_name(xor, p, q) def check(value: T_contra) -> bool: return p(value) ^ q(value) return check def all_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_contra]: """Create a new predicate that succeeds when all of the given predicates succeed.""" predicates = tuple(predicates) @bind_name(all_of, *predicates) def check(value: T_contra) -> bool: return all(p(value) for p in predicates) return check def any_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_contra]: """ Create a new predicate that succeeds when at least one of the given predicates succeed. """ predicates = tuple(predicates) @bind_name(any_of, *predicates) def check(value: T_contra) -> bool: return any(p(value) for p in predicates) return check def one_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_contra]: """ Create a new predicate that succeeds when exactly one of the given predicates succeed. """ predicates = tuple(predicates) @bind_name(one_of, *predicates) def check(value: T_contra) -> bool: return sum(p(value) for p in predicates) == 1 return check ================================================ FILE: src/phantom/predicates/collection.py ================================================ from collections.abc import Container from collections.abc import Iterable from collections.abc import Sized from typing import TypeVar from . import Predicate from ._utils import bind_name def contains(value: object) -> Predicate[Container]: """Create a new predicate that succeeds when its argument contains ``value``.""" @bind_name(contains, value) def compare(container: Container) -> bool: return value in container return compare def contained(container: Container) -> Predicate[object]: """ Create a new predicate that succeeds when its argument is contained by ``container``. """ @bind_name(contained, container) def compare(value: object) -> bool: return value in container return compare def count(predicate: Predicate[int]) -> Predicate[Sized]: """ Create a predicate that succeeds when the size of its argument satisfies the given ``predicate``. """ @bind_name(count, predicate) def compare(sized: Sized) -> bool: return predicate(len(sized)) return compare _O = TypeVar("_O", bound=object) def exists(predicate: Predicate[_O]) -> Predicate[Iterable]: """ Create a predicate that succeeds when one or more items in its argument satisfies ``predicate``. """ @bind_name(exists, predicate) def compare(iterable: Iterable) -> bool: return any(predicate(item) for item in iterable) return compare def every(predicate: Predicate[_O]) -> Predicate[Iterable]: """ Create a predicate that succeeds when all items in its argument satisfy ``predicate``. """ @bind_name(every, predicate) def compare(iterable: Iterable) -> bool: return all(predicate(item) for item in iterable) return compare ================================================ FILE: src/phantom/predicates/datetime.py ================================================ import datetime from .boolean import negate def is_tz_aware(dt: datetime.datetime) -> bool: """Return :py:const:`True` if ``dt`` is timezone aware.""" # https://docs.python.org/3/library/datetime.html#determining-if-an-object-is-aware-or-naive return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None def is_tz_naive(dt: datetime.datetime) -> bool: """Return :py:const:`True` if ``dt`` is timezone naive.""" return negate(is_tz_aware)(dt) ================================================ FILE: src/phantom/predicates/generic.py ================================================ import typeguard from typeguard import CollectionCheckStrategy from typeguard import ForwardRefPolicy from . import Predicate from ._utils import bind_name def equal(a: object) -> Predicate[object]: """Create a new predicate that succeeds when its argument is equal to ``a``.""" @bind_name(equal, a) def check(b: object) -> bool: return a == b return check def identical(a: object) -> Predicate[object]: """Create a new predicate that succeeds when its argument is identical to ``a``.""" @bind_name(identical, a) def check(b: object) -> bool: return a is b return check def of_type(t: type | tuple[type, ...]) -> Predicate[object]: """ Create a new predicate that succeeds when its argument is an instance of ``t``. """ @bind_name(of_type, t) def check(a: object) -> bool: return isinstance(a, t) return check def of_complex_type(t: type) -> Predicate[object]: @bind_name(of_complex_type, t) def check(a: object) -> bool: try: typeguard.check_type( value=a, expected_type=t, typecheck_fail_callback=None, forward_ref_policy=ForwardRefPolicy.ERROR, collection_check_strategy=CollectionCheckStrategy.ALL_ITEMS, ) except typeguard.TypeCheckError: return False return True return check ================================================ FILE: src/phantom/predicates/interval.py ================================================ """ Functions that create new predicates that succeed when their argument is strictly or non strictly between the upper and lower bounds. There are corresponding phantom types that use these predicates in :py:mod:`phantom.interval`. """ from typing import TypeVar from phantom._utils.types import SupportsLeGe from phantom._utils.types import SupportsLeGt from phantom._utils.types import SupportsLtGe from phantom._utils.types import SupportsLtGt from ._base import Predicate from ._utils import bind_name T = TypeVar("T") def exclusive(low: T, high: T) -> Predicate[SupportsLtGt[T]]: """ Create a predicate that succeeds when its argument is in the range ``(low, high)``. """ @bind_name(exclusive, low, high) def check(value: SupportsLtGt[T]) -> bool: return low < value < high return check def exclusive_inclusive(low: T, high: T) -> Predicate[SupportsLeGt[T]]: """ Create a predicate that succeeds when its argument is in the range ``(low, high]``. """ @bind_name(exclusive_inclusive, low, high) def check(value: SupportsLeGt[T]) -> bool: return low < value <= high return check def inclusive_exclusive(low: T, high: T) -> Predicate[SupportsLtGe[T]]: """ Create a predicate that succeeds when its argument is in the range ``[low, high)``. """ @bind_name(inclusive_exclusive, low, high) def check(value: SupportsLtGe[T]) -> bool: return low <= value < high return check def inclusive(low: T, high: T) -> Predicate[SupportsLeGe[T]]: """ Create a predicate that succeeds when its argument is in the range ``[low, high]``. """ @bind_name(inclusive, low, high) def check(value: SupportsLeGe[T]) -> bool: return low <= value <= high return check ================================================ FILE: src/phantom/predicates/numeric.py ================================================ from typing import TypeVar from phantom._utils.types import SupportsGe from phantom._utils.types import SupportsGt from phantom._utils.types import SupportsLe from phantom._utils.types import SupportsLt from phantom._utils.types import SupportsMod from ._base import Predicate from ._utils import bind_name from .boolean import negate from .generic import equal T = TypeVar("T") U = TypeVar("U") def less(n: T) -> Predicate[SupportsLt[T]]: """ Create a new predicate that succeeds when its argument is strictly less than ``n``. """ @bind_name(less, n) def check(value: SupportsLt[T]) -> bool: return value < n return check def le(n: T) -> Predicate[SupportsLe[T]]: """ Create a new predicate that succeeds when its argument is less than or equal to ``n``. """ @bind_name(le, n) def check(value: SupportsLe[T]) -> bool: return value <= n return check def greater(n: T) -> Predicate[SupportsGt[T]]: """ Create a new predicate that succeeds when its argument is strictly greater than ``n``. """ @bind_name(greater, n) def check(value: SupportsGt[T]) -> bool: return value > n return check def ge(n: T) -> Predicate[SupportsGe[T]]: """ Create a new predicate that succeeds when its argument is greater than or equal to ``n``. """ @bind_name(ge, n) def check(value: SupportsGe[T]) -> bool: return value >= n return check def positive(n: SupportsGt[int]) -> bool: """Return :py:const:`True` when ``n`` is strictly greater than zero.""" return greater(0)(n) def non_positive(n: SupportsLe[int]) -> bool: """Return :py:const:`True` when ``n`` is less than or equal to zero.""" return le(0)(n) def negative(n: SupportsLt[int]) -> bool: """Return :py:const:`True` when ``n`` is strictly less than zero.""" return less(0)(n) def non_negative(n: SupportsGe[int]) -> bool: """Return :py:const:`True` when ``n`` is greater than or equal to zero.""" return ge(0)(n) def modulo(n: T, p: Predicate[U]) -> Predicate[SupportsMod[T, U]]: """ Create a new predicate that succeeds when its argument modulo ``n`` satisfies the given predicate ``p``. """ @bind_name(modulo, n, p) def check(value: SupportsMod[T, U]) -> bool: return p(value % n) return check def even(n: int) -> bool: """Return :py:const:`True` when ``n`` is even.""" return modulo(2, equal(0))(n) def odd(n: int) -> bool: """Return :py:const:`True` when ``n`` is odd.""" return negate(even)(n) ================================================ FILE: src/phantom/predicates/re.py ================================================ from re import Pattern from . import Predicate from ._utils import bind_name def is_match(pattern: Pattern[str]) -> Predicate[str]: """ Create a new predicate that succeeds when the start of its argument matches the given ``pattern``. """ @bind_name(is_match, pattern.pattern) def match(instance: str) -> bool: return pattern.match(instance) is not None return match def is_full_match(pattern: Pattern[str]) -> Predicate[str]: """ Create a new predicate that succeeds when its whole argument matches the given ``pattern``. """ @bind_name(is_full_match, pattern.pattern) def full_match(instance: str) -> bool: return pattern.fullmatch(instance) is not None return full_match ================================================ FILE: src/phantom/py.typed ================================================ ================================================ FILE: src/phantom/re.py ================================================ """ Types for representing strings that match a pattern. .. code-block:: python class Greeting(Match, pattern=r"^(Hi|Hello)"): ... assert isinstance("Hello Jane!", Greeting) """ from __future__ import annotations import re from re import Pattern from typing import Any from . import Phantom from . import _hypothesis from ._utils.misc import resolve_class_attr from .predicates.re import is_full_match from .predicates.re import is_match from .schema import Schema __all__ = ("Match", "FullMatch") def _compile(pattern: Pattern[str] | str) -> Pattern[str]: if not isinstance(pattern, Pattern): return re.compile(pattern) return pattern class Match(str, Phantom, abstract=True): """ Takes ``pattern: Pattern[str] | str`` as class argument as either a compiled :py:class:`Pattern` or a :py:class:`str` to be compiled. Uses the :py:func:`phantom.predicates.re.is_match` predicate. """ __pattern__: Pattern[str] def __init_subclass__(cls, pattern: Pattern[str] | str, **kwargs: Any) -> None: resolve_class_attr(cls, "__pattern__", _compile(pattern)) super().__init_subclass__(predicate=is_match(cls.__pattern__), **kwargs) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": ( "A string starting with a match of the format regular expression." ), "format": str(cls.__pattern__.pattern), } class FullMatch(str, Phantom, abstract=True): """ Takes ``pattern: Pattern[str] | str`` as class argument as either a compiled :py:class:`Pattern` or a :py:class:`str` to be compiled. Uses the :py:func:`phantom.predicates.re.is_full_match` predicate. """ __pattern__: Pattern[str] def __init_subclass__(cls, pattern: Pattern[str] | str, **kwargs: Any) -> None: resolve_class_attr(cls, "__pattern__", _compile(pattern)) super().__init_subclass__(predicate=is_full_match(cls.__pattern__), **kwargs) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A string that matches the format regular expression.", "format": str(cls.__pattern__.pattern), } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None: from hypothesis.strategies import from_regex return from_regex(cls.__pattern__, fullmatch=True) ================================================ FILE: src/phantom/schema.py ================================================ from collections.abc import Sequence from typing import Literal from typing_extensions import TypedDict from typing_extensions import final class Schema(TypedDict, total=False): title: str description: str type: Literal["array", "string", "float", "number"] format: str examples: Sequence[object] minimum: float | None maximum: float | None exclusiveMinimum: float | None exclusiveMaximum: float | None minItems: int | None maxItems: int | None minLength: int | None maxLength: int | None class SchemaField: @classmethod @final def __modify_schema__(cls, field_schema: dict) -> None: """ This final method is called by pydantic and collects overrides from :func:`Phantom.__schema__() `. Override :func:`__schema__() ` to provide custom schema representations for phantom types. """ field_schema.update( {key: value for key, value in cls.__schema__().items() if value is not None} ) @classmethod def __schema__(cls) -> Schema: """ Hook for providing schema metadata. Override in subclasses to customize a types schema representation. See pydantic's documentation on ``__modify_schema__()`` for more information. This hook differs to pydantic's ``__modify_schema__()`` and expects subclasses to instantiate new dicts instead of mutating a given one. Example: .. code-block:: python class Name(str, Phantom, predicate=...): @classmethod def __schema__(cls): return {**super().__schema__(), "description": "A name type"} """ return {"title": cls.__name__} ================================================ FILE: src/phantom/sized.py ================================================ """ Types describing collections with size boundaries. These types should only be used with immutable collections. There is a naive check that eliminates some of the most common mutable collections in the instance check. However, a guaranteed check is probably impossible to implement, so some amount of developer discipline is required. Sized types are created by subclassing :py:class:`PhantomBound` and providing a minimum, maximum, or both as the ``min`` and ``max`` class arguments. For instance, :py:class:`NonEmpty` is implemented using ``min=1``. This made-up type would describe sized collections with between 5 and 10 ints: .. code-block:: python class SpecificSize(PhantomBound[int], min=5, max=10): ... This example creates a type that accepts strings with 255 or less characters: .. code-block:: python class SizedStr(str, PhantomBound[str], max=255): ... """ from __future__ import annotations from collections.abc import Iterable from collections.abc import Sized # This is the closest I could find to documentation of _ProtocolMeta. # https://github.com/python/cpython/commit/74d7f76e2c953fbfdb7ce01b7319d91d471cc5ef from typing import Any from typing import Generic from typing import Protocol from typing import TypeVar from typing import _ProtocolMeta from typing import get_args from typing import runtime_checkable from . import Phantom from . import PhantomMeta from . import Predicate from . import _hypothesis from ._utils.misc import is_not_known_mutable_instance from .predicates import boolean from .predicates import collection from .predicates import interval from .predicates import numeric from .schema import Schema __all__ = ( "SizedIterable", "PhantomSized", "PhantomBound", "NonEmpty", "NonEmptyStr", "Empty", ) T = TypeVar("T", bound=object, covariant=True) @runtime_checkable class SizedIterable(Sized, Iterable[T], Protocol[T]): """Intersection of :py:class:`typing.Sized` and :py:class:`typing.Iterable`.""" class SizedIterablePhantomMeta(PhantomMeta, _ProtocolMeta): ... class PhantomSized( Phantom[Sized], SizedIterable[T], Generic[T], metaclass=SizedIterablePhantomMeta, bound=SizedIterable, abstract=True, ): """ Takes class argument ``len: Predicate[int]``. Discouraged in favor of :py:class:`PhantomBound`, which better supports automatic schema generation. """ def __init_subclass__( cls, len: Predicate[int], # noqa: A002 **kwargs: Any, ) -> None: super().__init_subclass__( predicate=boolean.both( is_not_known_mutable_instance, collection.count(len), ), **kwargs, ) @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "type": "array", } class UnresolvedBounds(Exception): ... class LSPViolation(Exception): ... class PhantomBound( Phantom[Sized], SizedIterable[T], Generic[T], metaclass=SizedIterablePhantomMeta, bound=SizedIterable, abstract=True, ): """Takes class arguments ``min: int``, ``max: int``.""" __min__: int | None __max__: int | None def __init_subclass__( cls, min: int | None = None, # noqa: A002 max: int | None = None, # noqa: A002 abstract: bool = False, **kwargs: Any, ) -> None: inherited_min = getattr(cls, "__min__", None) inherited_max = getattr(cls, "__max__", None) cls.__min__ = inherited_min if min is None else min cls.__max__ = inherited_max if max is None else max # Note: There's possibly value in generalizing this, to be able to declaratively # describe the relationship between an attribute and its inherited value. if ( cls.__min__ is not None and inherited_min is not None and cls.__min__ < inherited_min ): raise LSPViolation( f"Cannot set a smaller min than inherited ({cls.__min__} < " f"{inherited_min})." ) if ( cls.__max__ is not None and inherited_max is not None and cls.__max__ > inherited_max ): raise LSPViolation( f"Cannot set a larger max than inherited ({cls.__max__} > " f"{inherited_max})." ) if cls.__min__ is not None and cls.__max__ is not None: size = interval.inclusive(cls.__min__, cls.__max__) elif cls.__min__ is not None: size = numeric.ge(cls.__min__) elif cls.__max__ is not None: size = numeric.le(cls.__max__) elif abstract: super().__init_subclass__(abstract=abstract, **kwargs) return else: raise UnresolvedBounds( f"Concrete type {cls.__qualname__} must provide either min or max, or " f"both." ) super().__init_subclass__( predicate=boolean.both( is_not_known_mutable_instance, collection.count(size), ), abstract=abstract, **kwargs, ) @classmethod def __schema__(cls) -> Schema: return ( { **super().__schema__(), "type": "string", "minLength": cls.__min__, "maxLength": cls.__max__, } if str in cls.__mro__ else { **super().__schema__(), "type": "array", "minItems": cls.__min__, "maxItems": cls.__max__, } ) @classmethod def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy: from hypothesis.strategies import DrawFn from hypothesis.strategies import composite from hypothesis.strategies import from_type from hypothesis.strategies import lists from hypothesis.strategies import text def create_strategy(type_: type[T]) -> _hypothesis.SearchStrategy[T] | None: min_size = cls.__min__ or 0 if cls.__bound__ is str: return text( # type: ignore[return-value] min_size=min_size, max_size=cls.__max__, ) (inner_type,) = get_args(type_) @composite def tuples(draw: DrawFn) -> tuple: strategy = lists( from_type(inner_type), min_size=min_size, max_size=cls.__max__, ) return tuple(draw(strategy)) return tuples() # type: ignore[return-value] return create_strategy class NonEmpty(PhantomBound[T], Generic[T], min=1): """A sized collection with at least one item.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A non-empty array.", } class NonEmptyStr(str, NonEmpty[str]): """A sized str with at least one character.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "A non-empty string.", } class Empty(PhantomBound[T], Generic[T], max=0): """A sized collection with exactly zero items.""" @classmethod def __schema__(cls) -> Schema: return { **super().__schema__(), "description": "An empty array.", } @classmethod def __register_strategy__(cls) -> _hypothesis.SearchStrategy: from hypothesis.strategies import just return just(()) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/ext/__init__.py ================================================ ================================================ FILE: tests/ext/test_hypothesis.py ================================================ from __future__ import annotations from dataclasses import dataclass from dataclasses import fields from functools import total_ordering from typing import Generic from typing import TypeVar from typing import get_origin from typing import get_type_hints from hypothesis import given from hypothesis import settings from hypothesis.strategies import builds from phantom.boolean import Falsy from phantom.boolean import Truthy from phantom.datetime import TZAware from phantom.datetime import TZNaive from phantom.interval import Exclusive from phantom.interval import ExclusiveInclusive from phantom.interval import Inclusive from phantom.interval import InclusiveExclusive from phantom.interval import Natural from phantom.interval import NegativeInt from phantom.interval import Portion from phantom.iso3166 import ParsedAlpha2 from phantom.negated import SequenceNotStr from phantom.re import FullMatch from phantom.sized import Empty from phantom.sized import NonEmpty from phantom.sized import NonEmptyStr from phantom.sized import PhantomBound from tests.types import FloatExc from tests.types import FloatExcInc from tests.types import FloatInc from tests.types import FloatIncExc from tests.types import IntExc from tests.types import IntExcInc from tests.types import IntInc from tests.types import IntIncExc class TensFloat(float, InclusiveExclusive, low=10, high=20): ... class TensInt(int, InclusiveExclusive, low=10, high=20): ... class Url( FullMatch, pattern=r"https?://www\.[A-z]+\.(com|se|org)", ): ... T = TypeVar("T", bound=object, covariant=True) class Few(PhantomBound[T], Generic[T], min=5, max=15): ... @total_ordering class Inf: def __eq__(self, other): return False def __lt__(self, other): return False # Test can create types that don't map to a Hypothesis strategy. class InmappableInc(int, Inclusive, low=Inf(), high=100): ... class InmappableExc(float, Exclusive, low=Inf(), high=100): ... class InmappableIncExc(int, InclusiveExclusive, low=Inf(), high=100): ... class InmappableExcInc(float, ExclusiveInclusive, low=Inf(), high=100): ... @dataclass class Model: tz_aware: TZAware tz_naive: TZNaive truthy: Truthy falsy: Falsy tens_float: TensFloat tens_int: TensInt natural: Natural negative_int: NegativeInt portion: Portion float_inc: FloatInc int_inc: IntInc float_exc: FloatExc int_exc: IntExc float_inc_exc: FloatIncExc int_inc_exc: IntIncExc float_exc_inc: FloatExcInc int_exc_inc: IntExcInc parsed_alpha_2: ParsedAlpha2 url: Url not_str: SequenceNotStr[str | int] non_empty: NonEmpty[int] non_empty_str: NonEmptyStr empty: Empty ints: Few[int] mixed: Few[int | str] @given(builds(Model)) @settings(max_examples=10) def test_can_generate_hypothesis_values(model: Model) -> None: hints = get_type_hints(Model) for field in fields(Model): type_ = hints[field.name] inner_type = get_origin(type_) or type_ assert isinstance(getattr(model, field.name), inner_type) ================================================ FILE: tests/ext/test_phonenumbers.py ================================================ import pytest from typing_extensions import assert_type from phantom.errors import BoundError from phantom.ext.phonenumbers import FormattedPhoneNumber from phantom.ext.phonenumbers import InvalidPhoneNumber from phantom.ext.phonenumbers import PhoneNumber from phantom.ext.phonenumbers import _deconstruct_phone_number from phantom.ext.phonenumbers import is_formatted_phone_number from phantom.ext.phonenumbers import is_phone_number from phantom.ext.phonenumbers import normalize_phone_number pytestmark = [pytest.mark.external] class TestPhoneNumber: def test_unparsable_number_is_not_instance(self): assert not isinstance("+46", PhoneNumber) def test_invalid_number_is_not_instance(self): assert not isinstance("+467012345678", PhoneNumber) def test_unformatted_number_is_instance(self): assert isinstance("+46 (701) 234567", PhoneNumber) class TestFormattedPhoneNumber: def test_unparsable_number_is_not_instance(self): assert not isinstance("+46", FormattedPhoneNumber) def test_invalid_number_is_not_instance(self): assert not isinstance("+467012345678", FormattedPhoneNumber) def test_unformatted_number_is_not_instance(self): assert not isinstance("+46 (701) 234567", FormattedPhoneNumber) def test_formatted_number_is_instance(self): assert isinstance("+46701234567", FormattedPhoneNumber) def test_normalizes_unformatted_number(self): number = FormattedPhoneNumber.parse("+46 (701) 234567") assert number == "+46701234567" assert isinstance(number, FormattedPhoneNumber) def test_parse_raises_for_invalid_phone_number(self): with pytest.raises(InvalidPhoneNumber): FormattedPhoneNumber.parse("+46") def test_raises_type_error_for_out_of_bound_type(self): """Since we override parse we need to test the bound check""" value = 123 with pytest.raises( BoundError, match=rf"Value is not within bound of 'str': {value}" ): FormattedPhoneNumber.parse(123) class TestDeconstructPhoneNumber: def test_can_parse_international_phone_number_without_country_code(self): result = _deconstruct_phone_number("123456789", "SE") assert result.country_code == 46 assert result.national_number == 123456789 def test_can_parse_international_phone_number_with_country_code(self): result = _deconstruct_phone_number("+46123456789", "NO") assert result.country_code == 46 assert result.national_number == 123456789 def test_can_parse_national_phone_number_with_country_code(self): result = _deconstruct_phone_number("0123456789", "SE") assert result.country_code == 46 assert result.national_number == 123456789 def test_raises_invalid_phone_number_for_insufficient_country_data(self): with pytest.raises(InvalidPhoneNumber) as exc_info: _deconstruct_phone_number("0701") assert exc_info.value.error_type == InvalidPhoneNumber.INVALID_COUNTRY_CODE def test_raises_invalid_phone_number_for_parse_exception(self): with pytest.raises(InvalidPhoneNumber) as exc_info: _deconstruct_phone_number("0") assert exc_info.value.error_type == InvalidPhoneNumber.NOT_A_NUMBER def test_raises_invalid_phone_number_for_invalid_phone_number(self): with pytest.raises(InvalidPhoneNumber) as exc_info: _deconstruct_phone_number("+461234567890") assert exc_info.value.error_type == InvalidPhoneNumber.INVALID class TestNormalizePhoneNumber: def test_can_normalize_national_number_with_country_code(self): assert normalize_phone_number("(123) 456 789", "SE") == "+46123456789" def test_can_normalize_international_number_without_country_code(self): assert normalize_phone_number("+46 (123) 456 789") == "+46123456789" class TestIsPhoneNumber: def test_returns_true_for_valid_number(self): assert is_phone_number("+46 (123) 456789") is True def test_returns_false_for_invalid_number(self): assert is_phone_number("+461234567890") is False class TestIsFormattedPhoneNumber: def test_returns_true_for_formatted_number(self) -> None: value = "+46123456789" assert is_formatted_phone_number(value) assert_type(value, FormattedPhoneNumber) def test_returns_false_for_unformatted_number(self): assert is_formatted_phone_number("+46 (123) 456 789") is False ================================================ FILE: tests/ext/test_phonenumbers.yaml ================================================ - case: bound_is_not_subtype main: | from phantom.ext.phonenumbers import PhoneNumber from phantom.ext.phonenumbers import FormattedPhoneNumber def takes_phone_number(n: PhoneNumber) -> None: ... takes_phone_number("") # E: Argument 1 to "takes_phone_number" has incompatible type "str"; expected "PhoneNumber" [arg-type] def takes_formatted_phone_number(n: FormattedPhoneNumber) -> None: ... takes_formatted_phone_number("") # E: Argument 1 to "takes_formatted_phone_number" has incompatible type "str"; expected "FormattedPhoneNumber" [arg-type] - case: can_instantiate main: | from phantom.ext.phonenumbers import PhoneNumber from phantom.ext.phonenumbers import FormattedPhoneNumber def takes_phone_number(n: PhoneNumber) -> None: ... takes_phone_number(PhoneNumber.parse("")) def takes_formatted_phone_number(n: FormattedPhoneNumber) -> None: ... takes_formatted_phone_number(FormattedPhoneNumber.parse("")) - case: can_infer main: | from phantom.ext.phonenumbers import PhoneNumber from phantom.ext.phonenumbers import FormattedPhoneNumber def takes_phone_number(n: PhoneNumber) -> None: ... n = "" assert isinstance(n, PhoneNumber) takes_phone_number(n) def takes_formatted_phone_number(n: FormattedPhoneNumber) -> None: ... f = "" assert isinstance(f, FormattedPhoneNumber) takes_formatted_phone_number(f) ================================================ FILE: tests/predicates/__init__.py ================================================ import pytest pytest.register_assert_rewrite("tests.predicates.utils") ================================================ FILE: tests/predicates/test_boolean.py ================================================ from collections.abc import Iterable import pytest from phantom import Predicate from phantom.predicates import boolean from .utils import assert_predicate_name_equals class TestTrue: @pytest.mark.parametrize("value", [0, False, 1, "test", ()]) def test_returns_true_for_any_given_value(self, value: object) -> None: assert boolean.true(value) is True class TestFalse: @pytest.mark.parametrize("value", [0, False, 1, "test", ()]) def test_returns_false_for_any_given_value(self, value: object) -> None: assert boolean.false(value) is False class TestNegate: def test_can_negate_true(self): assert boolean.negate(boolean.true)(object()) is False def test_can_negate_false(self): assert boolean.negate(boolean.false)(0) is True def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(boolean.negate(boolean.true), "negate(true)") parametrize_truthy = pytest.mark.parametrize("value", [1, "a", (0,), [""], True]) parametrize_falsy = pytest.mark.parametrize("value", [0, "", (), [], False]) class TestTruthy: @parametrize_truthy def test_returns_true_for_truthy_value(self, value: object) -> None: assert boolean.truthy(value) is True @parametrize_falsy def test_returns_false_for_falsy_value(self, value: object) -> None: assert boolean.truthy(value) is False class TestFalsy: @parametrize_falsy def test_returns_true_for_falsy_value(self, value: object) -> None: assert boolean.falsy(value) is True @parametrize_truthy def test_returns_false_for_truthy_value(self, value: object) -> None: assert boolean.falsy(value) is False class TestBoth: def test_returns_true_for_two_succeeding_predicates(self) -> None: assert boolean.both(boolean.true, boolean.true)(0) is True @pytest.mark.parametrize( "a, b", [ (boolean.false, boolean.true), (boolean.true, boolean.false), (boolean.false, boolean.false), ], ) def test_returns_false_for_falsy_predicate( self, a: Predicate, b: Predicate ) -> None: assert boolean.both(a, b)(0) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.both(boolean.true, boolean.false), "both(true, false)" ) class TestEither: @pytest.mark.parametrize( "a, b", [ (boolean.false, boolean.true), (boolean.true, boolean.false), (boolean.true, boolean.true), ], ) def test_returns_true_for_truthy_predicate( self, a: Predicate, b: Predicate ) -> None: assert boolean.either(a, b)(0) is True def test_returns_false_for_two_falsy_predicates(self) -> None: assert boolean.either(boolean.false, boolean.false)(0) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.either(boolean.false, boolean.true), "either(false, true)" ) class TestXor: @pytest.mark.parametrize( "a, b", [ (boolean.false, boolean.true), (boolean.true, boolean.false), ], ) def test_returns_true_for_two_different_bools( self, a: Predicate, b: Predicate ) -> None: assert boolean.xor(a, b)(0) is True @pytest.mark.parametrize( "a, b", [ (boolean.false, boolean.false), (boolean.true, boolean.true), ], ) def test_returns_false_for_two_equal_bools( self, a: Predicate, b: Predicate ) -> None: assert boolean.xor(a, b)(0) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.xor(boolean.true, boolean.false), "xor(true, false)" ) parametrize_all_true = pytest.mark.parametrize( "predicates", [ (boolean.true,), (boolean.true, boolean.true), (boolean.true, boolean.true, boolean.true), ], ) parametrize_some_false = pytest.mark.parametrize( "predicates", [ (boolean.true, boolean.false), (boolean.false, boolean.true), (boolean.true, boolean.false, boolean.true), ], ) parametrize_all_false = pytest.mark.parametrize( "predicates", [ (boolean.false,), (boolean.false, boolean.false), (boolean.false, boolean.false, boolean.false), ], ) class TestAllOf: def test_returns_true_for_empty_set_of_predicates(self) -> None: predicate: Predicate[int] = boolean.all_of(()) assert predicate(0) is True @parametrize_all_true def test_returns_true_for_succeeding_predicates( self, predicates: Iterable[Predicate] ) -> None: assert boolean.all_of(predicates)(0) is True @parametrize_some_false def test_returns_false_for_some_failing_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.all_of(predicates)(0) is False @parametrize_all_false def test_returns_false_for_only_failing_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.all_of(predicates)(0) is False @parametrize_some_false def test_materializes_generated_predicates( self, predicates: Iterable[Predicate] ) -> None: predicate = boolean.all_of(predicate for predicate in predicates) assert predicate(0) is False assert predicate(0) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.all_of([boolean.true, boolean.false]), "all_of(true, false)" ) class TestAnyOf: def test_returns_false_for_empty_set_of_predicates(self) -> None: predicate: Predicate[int] = boolean.any_of(()) assert predicate(0) is False @parametrize_all_true def test_returns_true_for_succeeding_predicates( self, predicates: Iterable[Predicate] ) -> None: assert boolean.any_of(predicates)(0) is True @parametrize_some_false def test_returns_true_for_some_failing_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.any_of(predicates)(0) is True @parametrize_all_false def test_returns_false_for_only_failing_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.any_of(predicates)(0) is False @parametrize_some_false def test_materializes_generated_predicates( self, predicates: Iterable[Predicate] ) -> None: predicate = boolean.any_of(predicate for predicate in predicates) assert predicate(0) is True assert predicate(0) is True def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.any_of([boolean.false, boolean.true]), "any_of(false, true)" ) class TestOneOf: def test_returns_false_for_empty_set_of_predicates(self) -> None: predicate: Predicate[int] = boolean.one_of(()) assert predicate(0) is False @pytest.mark.parametrize( "predicates", [ (boolean.true, boolean.true), (boolean.true, boolean.true, boolean.true), (boolean.false, boolean.true, boolean.true), (boolean.true, boolean.true, boolean.false), (boolean.true, boolean.false, boolean.true), ], ) def test_returns_false_for_more_than_one_succeeding_predicates( self, predicates: Iterable[Predicate] ) -> None: assert boolean.one_of(predicates)(0) is False @pytest.mark.parametrize( "predicates", [ (boolean.true,), (boolean.true, boolean.false), (boolean.false, boolean.true), (boolean.true, boolean.false, boolean.false), (boolean.false, boolean.true, boolean.false), (boolean.false, boolean.false, boolean.true), ], ) def test_returns_true_for_one_succeeding_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.one_of(predicates)(0) is True @parametrize_all_false def test_returns_false_for_only_failing_predicate( self, predicates: Iterable[Predicate] ) -> None: assert boolean.one_of(predicates)(0) is False @pytest.mark.parametrize( "predicates", [ (boolean.true,), (boolean.false, boolean.true), (boolean.false, boolean.true, boolean.false), ], ) def test_materializes_generated_predicates( self, predicates: Iterable[Predicate] ) -> None: predicate = boolean.one_of(predicate for predicate in predicates) assert predicate(0) is True assert predicate(0) is True def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( boolean.one_of([boolean.true, boolean.false]), "one_of(true, false)" ) ================================================ FILE: tests/predicates/test_collection.py ================================================ from collections.abc import Container from collections.abc import Iterable from collections.abc import Sized import pytest from phantom import Predicate from phantom.predicates import collection from phantom.predicates import generic from phantom.predicates import numeric from .utils import assert_predicate_name_equals class TestContains: @pytest.mark.parametrize( "item, container", [ (1, (1, 2)), ("b", "abc"), ], ) def test_returns_true_for_container_with_item( self, item: object, container: Container ) -> None: assert collection.contains(item)(container) is True @pytest.mark.parametrize( "item, container", [ (1, (2, 3)), ("d", "abc"), ], ) def test_returns_false_for_container_without_item( self, item: object, container: Container ) -> None: assert collection.contains(item)(container) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( collection.contains("needle"), "contains('needle')" ) class TestContained: @pytest.mark.parametrize( "container, item", [ ((1, 2), 1), ("abc", "b"), ], ) def test_returns_true_for_item_in_container( self, container: Container, item: object ) -> None: assert collection.contained(container)(item) is True @pytest.mark.parametrize( "container, item", [ ((2, 3), 1), ("abc", "d"), ], ) def test_returns_false_for_item_not_in_container( self, container: Container, item: object ) -> None: assert collection.contained(container)(item) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( collection.contained((1, 2, 3)), "contained((1, 2, 3))" ) class TestCount: @pytest.mark.parametrize( "predicate, sized", [ (generic.equal(1), [0]), (numeric.le(3), ("a", "b")), (numeric.ge(3), "abc"), ], ) def test_returns_true_for_size_matching_predicate( self, predicate: Predicate, sized: Sized ) -> None: assert collection.count(predicate)(sized) is True @pytest.mark.parametrize( "predicate, sized", [ (generic.equal(1), [0, 0]), (numeric.le(3), ("a", "b", 1, 2)), (numeric.ge(3), "ab"), ], ) def test_returns_false_for_size_failing_predicate( self, predicate: Predicate, sized: Sized ) -> None: assert collection.count(predicate)(sized) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(collection.count(numeric.ge(3)), "count(ge(3))") class TestExists: @pytest.mark.parametrize( "predicate, iterable", [ (generic.identical(...), ["a", "b", ...]), (numeric.greater(0), [-3, 0, 1, 0]), (generic.equal("a"), "cbad"), ], ) def test_returns_true_for_iterable_containing_satisfying_item( self, predicate: Predicate[object], iterable: Iterable ) -> None: assert collection.exists(predicate)(iterable) is True @pytest.mark.parametrize( "predicate, iterable", [ (generic.identical(...), ["a", "b", "c"]), (numeric.greater(1), [-3, 0, 1, 0]), (generic.equal("a"), "cbed"), ], ) def test_returns_false_for_iterable_not_containing_satisfying_item( self, predicate: Predicate[object], iterable: Iterable ) -> None: assert collection.exists(predicate)(iterable) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( collection.exists(numeric.less(-3)), "exists(less(-3))" ) class TestEvery: @pytest.mark.parametrize( "predicate, iterable", [ (generic.identical(1), [1, 1, 1]), (generic.identical(1), []), (numeric.greater(0), [1, 2]), (generic.equal("a"), "aa"), ], ) def test_returns_true_for_complete_iterable( self, predicate: Predicate[object], iterable: Iterable ) -> None: assert collection.every(predicate)(iterable) is True @pytest.mark.parametrize( "predicate, iterable", [ (generic.identical(...), [..., "b", "c"]), (numeric.greater(1), [-3, 0, 1, 0, 2]), (generic.equal("a"), "abc"), ], ) def test_returns_false_for_incomplete_iterable( self, predicate: Predicate[object], iterable: Iterable ) -> None: assert collection.every(predicate)(iterable) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( collection.every(numeric.greater(42)), "every(greater(42))" ) ================================================ FILE: tests/predicates/test_datetime.py ================================================ import datetime import pytest from phantom.predicates.datetime import is_tz_aware from phantom.predicates.datetime import is_tz_naive parametrize_aware = pytest.mark.parametrize( "dt", (datetime.datetime.now(tz=datetime.timezone.utc),) ) parametrize_naive = pytest.mark.parametrize( "dt", (datetime.datetime.now(), datetime.datetime(1969, 12, 23)) ) class TestIsTZAware: @parametrize_aware def test_returns_true_for_aware_dt(self, dt: datetime.datetime) -> None: assert is_tz_aware(dt) is True @parametrize_naive def test_returns_false_for_naive_dt(self, dt: datetime.datetime) -> None: assert is_tz_aware(dt) is False class TestIsTZNaive: @parametrize_naive def test_returns_true_for_naive_dt(self, dt: datetime.datetime) -> None: assert is_tz_naive(dt) is True @parametrize_aware def test_returns_false_for_aware_dt(self, dt: datetime.datetime) -> None: assert is_tz_naive(dt) is False ================================================ FILE: tests/predicates/test_generic.py ================================================ import pytest from phantom.predicates import generic from .utils import assert_predicate_name_equals class TestEqual: @pytest.mark.parametrize("a, b", [(1, 1.0), (False, 0), (True, 1)]) def test_returns_true_for_equal_values(self, a: object, b: object) -> None: assert generic.equal(a)(b) is True assert generic.equal(b)(a) is True @pytest.mark.parametrize("a, b", [(2, 1), (1, 1.1), (True, False)]) def test_returns_false_for_non_equal_values(self, a: object, b: object) -> None: assert generic.equal(a)(b) is False assert generic.equal(b)(a) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(generic.equal("hello"), "equal('hello')") class TestIdentical: @pytest.mark.parametrize( "a, b", [(1, 1), ("a", "a"), (True, True), (False, False), ((), ())] ) def test_returns_true_for_identical_values(self, a: object, b: object) -> None: assert generic.identical(a)(b) is True assert generic.identical(b)(a) is True @pytest.mark.parametrize("a, b", [([], []), (1, 1.0), (False, 0), (True, 1)]) def test_returns_false_for_different_values(self, a: object, b: object) -> None: assert generic.identical(a)(b) is False assert generic.identical(b)(a) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(generic.identical("hello"), "identical('hello')") class TestOfType: @pytest.mark.parametrize("instance,types", [(1, int), (1, (int, float))]) def test_returns_true_for_instance_of_types( self, instance: object, types: type | tuple[type, ...], ) -> None: assert generic.of_type(types)(instance) is True @pytest.mark.parametrize("instance,types", [(1, float), ("", (int, float))]) def test_returns_false_for_instance_of_other_type( self, instance: object, types: type | tuple[type, ...], ) -> None: assert generic.of_type(types)(instance) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(generic.of_type(int), "of_type(int)") ================================================ FILE: tests/predicates/test_interval.py ================================================ from typing import TypeAlias import pytest from phantom.predicates import interval from .utils import assert_predicate_name_equals Boundaries: TypeAlias = tuple[float, float] parametrize_inside = pytest.mark.parametrize( "value, boundaries", [ (1.0001, (1, 2)), (1.9999, (1, 2)), (1.00001, (1.0, 2)), (1.99999, (1, 2.0)), ], ) parametrize_on_edge = pytest.mark.parametrize( "value, boundaries", [ (1, (1, 2)), (2, (1, 2)), (1.0, (1.0, 2)), (2.0, (1, 2.0)), ], ) parametrize_outside = pytest.mark.parametrize( "value, boundaries", [ (0, (1, 2)), (3, (1, 2)), (0.99999, (1.0, 2)), (2.00001, (1, 2.0)), (20, (1, 3)), ], ) class TestExclusive: def test_returns_true_for_middle_value(self) -> None: assert interval.exclusive(1, 3)(2) is True @parametrize_inside def test_returns_true_for_inside_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.exclusive(*boundaries)(value) is True @parametrize_on_edge def test_returns_false_for_edge_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.exclusive(*boundaries)(value) is False @parametrize_outside def test_returns_false_for_outside_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.exclusive(*boundaries)(value) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(interval.exclusive(0, 1), "exclusive(0, 1)") class TestInclusive: def test_returns_true_for_middle_value(self) -> None: assert interval.inclusive(1, 3)(2) is True @parametrize_inside def test_returns_true_for_inside_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.inclusive(*boundaries)(value) is True @parametrize_on_edge def test_returns_true_for_edge_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.inclusive(*boundaries)(value) is True @parametrize_outside def test_returns_false_for_outside_value( self, value: float, boundaries: Boundaries ) -> None: assert interval.inclusive(*boundaries)(value) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(interval.inclusive(0, 1), "inclusive(0, 1)") class TestInclusiveExclusive: def test_returns_true_for_middle_value(self) -> None: assert interval.inclusive_exclusive(1, 3)(2) is True def test_lower_bound(self) -> None: intv = interval.inclusive_exclusive(5, 10) assert intv(5) is True assert intv(4.9999) is False def test_upper_bound(self) -> None: intv = interval.inclusive_exclusive(5, 10) assert intv(10) is False assert intv(9.9999) is True def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( interval.inclusive_exclusive(0, 1), "inclusive_exclusive(0, 1)" ) class TestExclusiveInclusive: def test_returns_true_for_middle_value(self) -> None: assert interval.exclusive_inclusive(1, 3)(2) is True def test_lower_bound(self) -> None: intv = interval.exclusive_inclusive(5, 10) assert intv(5.0001) is True assert intv(5) is False def test_upper_bound(self) -> None: intv = interval.exclusive_inclusive(5, 10) assert intv(10) is True assert intv(10.00001) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( interval.exclusive_inclusive(0, 1), "exclusive_inclusive(0, 1)" ) ================================================ FILE: tests/predicates/test_numeric.py ================================================ import pytest from phantom.predicates import numeric from .utils import assert_predicate_name_equals class TestLess: def test_returns_true_for_values_below_limit(self) -> None: predicate = numeric.less(10) assert predicate(9) is True assert predicate(9.999) is True assert predicate(-5) is True def test_returns_false_for_values_above_limit(self) -> None: predicate = numeric.less(10) assert predicate(10) is False assert predicate(11) is False assert predicate(123) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(numeric.less(100), "less(100)") class TestLE: def test_returns_true_for_values_below_limit(self) -> None: predicate = numeric.le(10) assert predicate(9) is True assert predicate(9.999) is True assert predicate(-5) is True assert predicate(10) is True def test_returns_false_for_values_above_limit(self) -> None: predicate = numeric.le(10) assert predicate(10.0001) is False assert predicate(11) is False assert predicate(123) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(numeric.le(-100), "le(-100)") class TestGreater: def test_returns_true_for_values_above_limit(self) -> None: predicate = numeric.greater(120) assert predicate(121) is True assert predicate(120.0001) is True assert predicate(1200) is True def test_returns_false_for_values_below_limit(self) -> None: predicate = numeric.greater(120) assert predicate(120) is False assert predicate(119.9999) is False assert predicate(-120) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(numeric.greater(10), "greater(10)") class TestGE: def test_returns_true_for_values_above_limit(self) -> None: predicate = numeric.ge(120) assert predicate(121) is True assert predicate(120.0001) is True assert predicate(1200) is True assert predicate(120) is True def test_returns_false_for_values_below_limit(self) -> None: predicate = numeric.ge(120) assert predicate(119.9999) is False assert predicate(-120) is False assert predicate(119) is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(numeric.ge(10.134), "ge(10.134)") class TestPositive: def test_limits(self) -> None: assert numeric.positive(0) is False assert numeric.positive(-1) is False assert numeric.positive(1) is True assert numeric.positive(0.0001) is True class TestNonPositive: def test_limits(self) -> None: assert numeric.non_positive(0) is True assert numeric.non_positive(-1) is True assert numeric.non_positive(1) is False assert numeric.non_positive(0.0001) is False class TestNegative: def test_limits(self) -> None: assert numeric.negative(0) is False assert numeric.negative(-1) is True assert numeric.negative(1) is False assert numeric.negative(-0.0001) is True class TestNonNegative: def test_limits(self) -> None: assert numeric.non_negative(0) is True assert numeric.non_negative(-1) is False assert numeric.non_negative(1) is True assert numeric.non_negative(-0.0001) is False class TestModulo: def test_repr_contains_bound_parameter(self): assert_predicate_name_equals( numeric.modulo(2, numeric.greater(99)), "modulo(2, greater(99))" ) parametrize_even = pytest.mark.parametrize("value", [-8296, 0, 2, 32, 9314]) parametrize_odd = pytest.mark.parametrize("value", [-13829, -1, 1, 31, 10023]) class TestEven: @parametrize_even def test_returns_true_for_even_value(self, value: int) -> None: assert numeric.even(value) is True @parametrize_odd def test_returns_false_for_odd_value(self, value: int) -> None: assert numeric.even(value) is False class TestOdd: @parametrize_odd def test_returns_true_for_odd_value(self, value: int) -> None: assert numeric.odd(value) is True @parametrize_even def test_returns_false_for_even_value(self, value: int) -> None: assert numeric.odd(value) is False ================================================ FILE: tests/predicates/test_re.py ================================================ import re from phantom.predicates.re import is_full_match from phantom.predicates.re import is_match from .utils import assert_predicate_name_equals pattern = re.compile("abc@") abc_match = is_match(pattern) abc_full_match = is_full_match(pattern) class TestIsMatch: def test_returns_true_for_matching_string(self) -> None: assert abc_match("abc@") is True assert abc_match("abc@ extra") is True def test_returns_false_for_non_matching_string(self) -> None: assert abc_match("abd@") is False assert abc_match("extra abc@") is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(abc_match, "is_match('abc@')") class TestIsFullMatch: def test_returns_true_for_matching_string(self) -> None: assert abc_full_match("abc@") is True def test_returns_false_for_non_matching_string(self) -> None: assert abc_full_match("abc@ extra") is False assert abc_full_match("abd@") is False assert abc_full_match("extra abc@") is False def test_repr_contains_bound_parameter(self): assert_predicate_name_equals(abc_full_match, "is_full_match('abc@')") ================================================ FILE: tests/predicates/test_utils.py ================================================ from functools import partial from phantom.predicates import boolean from .utils import assert_predicate_name_equals def foo(a: int, b: int, c: int) -> bool: return a > b > c class TestFunctionRepr: def test_explodes_partial_arguments(self): predicate = partial(foo, 10, b=5) assert_predicate_name_equals(boolean.negate(predicate), "negate(foo(10, b=5))") predicate = partial(foo, 23, c=31) assert_predicate_name_equals(boolean.negate(predicate), "negate(foo(23, c=31))") ================================================ FILE: tests/predicates/utils.py ================================================ from phantom import Predicate def assert_predicate_name_equals(predicate: Predicate, expected_name: str) -> None: assert predicate.__name__ == expected_name assert predicate.__qualname__ == expected_name assert repr(predicate).startswith(f" bool: return True assert isinstance("a", AlwaysTrue) is True ================================================ FILE: tests/test_boolean.py ================================================ import pytest from phantom.boolean import Falsy from phantom.boolean import Truthy parametrize_truthy = pytest.mark.parametrize("v", (object(), ("a",), 1, True)) parametrize_falsy = pytest.mark.parametrize("v", ((), 0, False)) class TestTruthy: @parametrize_truthy def test_truthy_value_is_instance(self, v): assert isinstance(v, Truthy) @parametrize_falsy def test_falsy_value_is_not_instance(self, v): assert not isinstance(v, Truthy) @parametrize_truthy def test_instantiation_returns_instance(self, v): assert v is Truthy.parse(v) @parametrize_falsy def test_instantiation_raises_for_falsy_value(self, v): with pytest.raises(TypeError): Truthy.parse(v) class TestFalsy: @parametrize_falsy def test_falsy_value_is_instance(self, v): assert isinstance(v, Falsy) @parametrize_truthy def test_truthy_value_is_not_instance(self, v): assert not isinstance(v, Falsy) @parametrize_falsy def test_instantiation_returns_instance(self, v): assert v is Falsy.parse(v) @parametrize_truthy def test_instantiation_raises_for_truthy_value(self, v): with pytest.raises(TypeError): Falsy.parse(v) ================================================ FILE: tests/test_datetime.py ================================================ import datetime import pytest from phantom import BoundError from phantom.datetime import TZAware from phantom.datetime import TZNaive from phantom.errors import MissingDependency parametrize_aware = pytest.mark.parametrize( "dt", ( datetime.datetime.now(tz=datetime.timezone.utc), datetime.datetime(1969, 12, 23, tzinfo=datetime.timezone.utc), datetime.datetime.min.replace(tzinfo=datetime.timezone.utc), datetime.datetime.max.replace(tzinfo=datetime.timezone.utc), ), ) parametrize_naive = pytest.mark.parametrize( "dt", ( datetime.datetime.now(), datetime.datetime(1969, 12, 23), datetime.datetime.min, datetime.datetime.max, ), ) parametrize_invalid_type = pytest.mark.parametrize( "value", ( object(), datetime.time(), datetime.timedelta(), 1, 1.1, (), # https://github.com/pydantic/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh float("inf"), float("-inf"), ), ) parametrize_invalid_str = pytest.mark.parametrize( "value", ( # https://github.com/pydantic/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh "infinity", "inf", "-infinity", "-inf", "2022-13-24", "20222-12-24", "2022-12-32", "foo", ), ) min_utc = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc) max_utc = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) parametrize_aware_str = pytest.mark.parametrize( "value, expected", [ (min_utc.isoformat(), min_utc), (max_utc.isoformat(), max_utc), ( "2022-09-24T10:40:20+00:00", datetime.datetime(2022, 9, 24, 10, 40, 20, 0, tzinfo=datetime.timezone.utc), ), ( "2022-09-24T10:40:20.779388+00:00", datetime.datetime( 2022, 9, 24, 10, 40, 20, 779388, tzinfo=datetime.timezone.utc ), ), ], ) parametrize_naive_str = pytest.mark.parametrize( "value, expected", [ (datetime.datetime.min.isoformat(), datetime.datetime.min), (datetime.datetime.max.isoformat(), datetime.datetime.max), ( "2022-09-24T10:40:20", datetime.datetime(2022, 9, 24, 10, 40, 20, 0), ), ( "2022-09-24T10:40:20.779388", datetime.datetime(2022, 9, 24, 10, 40, 20, 779388), ), ], ) class TestTZAware: @parametrize_aware def test_aware_datetime_is_instance(self, dt: datetime.datetime): assert isinstance(dt, TZAware) @parametrize_naive def test_naive_datetime_is_not_instance(self, dt: datetime.datetime): assert not isinstance(dt, TZAware) @parametrize_naive def test_instantiation_raises_for_naive_datetime_instance( self, dt: datetime.datetime ): with pytest.raises(TypeError): TZAware.parse(dt) @parametrize_aware def test_instantiation_returns_instance(self, dt: datetime.datetime): assert dt is TZAware.parse(dt) @parametrize_invalid_type def test_parse_rejects_non_str_object(self, value: object): with pytest.raises(BoundError): TZAware.parse(value) @pytest.mark.external @parametrize_invalid_str def test_parse_rejects_invalid_str(self, value: object): with pytest.raises(TypeError): TZAware.parse(value) @pytest.mark.external @parametrize_naive_str def test_parse_rejects_naive_str(self, value: str, expected: datetime.datetime): with pytest.raises(TypeError): TZAware.parse(value) @pytest.mark.external @parametrize_aware_str def test_can_parse_valid_str(self, value: str, expected: datetime.datetime): assert TZAware.parse(value) == expected @pytest.mark.no_external @parametrize_aware_str def test_parse_str_without_dateutil_raises_missing_dependency( self, value: str, expected: datetime.datetime, ): with pytest.raises(MissingDependency): TZAware.parse(value) class TestTZNaive: @parametrize_naive def test_naive_datetime_is_instance(self, dt: datetime.datetime): assert isinstance(dt, TZNaive) @parametrize_aware def test_aware_datetime_is_not_instance(self, dt: datetime.datetime): assert not isinstance(dt, TZNaive) @parametrize_aware def test_instantiation_raises_for_aware_datetime_instance( self, dt: datetime.datetime ): with pytest.raises(TypeError): TZNaive.parse(dt) @parametrize_naive def test_instantiation_returns_instance(self, dt: datetime.datetime): assert dt is TZNaive.parse(dt) @parametrize_invalid_type def test_parse_rejects_non_str_object(self, value: object): with pytest.raises(BoundError): TZNaive.parse(value) @pytest.mark.external @parametrize_invalid_str def test_parse_rejects_invalid_str(self, value: object): with pytest.raises(TypeError): TZNaive.parse(value) @pytest.mark.external @parametrize_aware_str def test_parse_rejects_aware_str(self, value: str, expected: datetime.datetime): with pytest.raises(TypeError): TZNaive.parse(value) @pytest.mark.external @parametrize_naive_str def test_can_parse_valid_str(self, value: str, expected: datetime.datetime): assert TZNaive.parse(value) == expected @pytest.mark.no_external @parametrize_naive_str def test_parse_str_without_dateutil_raises_missing_dependency( self, value: str, expected: datetime.datetime, ): with pytest.raises(MissingDependency): TZNaive.parse(value) ================================================ FILE: tests/test_datetime.yaml ================================================ - case: calling_function_with_unknown_raises main: | import datetime from phantom.datetime import TZAware def take_aware(a: TZAware) -> TZAware: return a take_aware(datetime.datetime.now()) # E: Argument 1 to "take_aware" has incompatible type "datetime"; expected "TZAware" [arg-type] - case: calling_function_with_known main: | import datetime from phantom.datetime import TZAware def take_aware(a: TZAware) -> TZAware: return a b = take_aware( TZAware.parse(datetime.datetime.now(tz=datetime.timezone.utc)) ) reveal_type(b) # N: Revealed type is "phantom.datetime.TZAware" - case: narrowing_to_tz_aware_makes_tzinfo_non_optional main: | import datetime from phantom.datetime import TZAware dt: datetime.datetime reveal_type(dt.tzinfo) # N: Revealed type is "datetime.tzinfo | None" assert isinstance(dt, TZAware) reveal_type(dt.tzinfo) # N: Revealed type is "datetime.tzinfo" - case: bound_is_not_erased main: | import datetime from phantom.datetime import TZAware, TZNaive o: datetime.datetime aware = TZAware.parse(o) naive = TZNaive.parse(o) reveal_type(aware.month) # N: Revealed type is "builtins.int" reveal_type(naive.month) # N: Revealed type is "builtins.int" ================================================ FILE: tests/test_fn.py ================================================ from __future__ import annotations from collections.abc import Callable from collections.abc import Sequence from functools import partial from operator import add from operator import attrgetter from operator import itemgetter from operator import mul from typing import TypeVar import pytest from phantom import Predicate from phantom.fn import _name from phantom.fn import compose2 from phantom.fn import excepts from phantom.predicates.boolean import both from phantom.predicates.collection import count from phantom.predicates.collection import every from phantom.predicates.generic import equal class Test_name: class Nested: def method(self): ... @pytest.mark.parametrize( "function, expected", [ (lambda: None, "Test_name."), (partial(int), "int"), (Nested.method, "Test_name.Nested.method"), (attrgetter("attrib"), "operator.attrgetter('attrib')"), (itemgetter("key"), "operator.itemgetter('key')"), ], ) def test_can_get_name_of(self, function: Callable, expected: str) -> None: assert _name(function) == expected def reversed_str(value: str) -> str: return "".join(reversed(value)) AA = TypeVar("AA") AR = TypeVar("AR") BA = TypeVar("BA") class TestCompose2: @pytest.mark.parametrize( "a, b, argument, expected, name", [ (str.title, reversed_str, "test", "Tset", "str.title∘reversed_str"), (reversed_str, str.title, "test", "tseT", "reversed_str∘str.title"), (partial(add, 7), partial(mul, 3), 5, 22, "add∘mul"), (partial(mul, 3), partial(add, 7), 5, 36, "mul∘add"), ], ) def test_can_compose_two( self, a: Callable[[AA], AR], b: Callable[[BA], AA], argument: BA, expected: AR, name: str, ) -> None: composed = compose2(a, b) assert composed(argument) == expected assert composed.__name__ == name def test_can_compose_complex_predicate(self) -> None: as_parts: Callable[[str], list[str]] = partial(str.split, sep=".") is_valid_parts: Predicate[Sequence[str]] = both( count(equal(3)), every(str.isidentifier), ) is_valid_name = compose2(is_valid_parts, as_parts) assert is_valid_name("three.part.name") is True assert is_valid_name("two.parts") is False assert is_valid_name("not identifier.not.valid") is False assert is_valid_name("") is False class BaseError(Exception): ... class Error(BaseError): ... class ErrorA(Error): ... class ErrorB(Error): ... def dummy_function(val: type[Exception]) -> None: if val is not None: raise val class TestExcepts: @pytest.mark.parametrize( "function, argument, return_value", [ (excepts(Error)(dummy_function), ErrorA, False), (excepts((ErrorA, ErrorB))(dummy_function), ErrorA, False), (excepts((ErrorA, ErrorB))(dummy_function), None, True), (excepts((ErrorA, ErrorB), negate=True)(dummy_function), None, False), (excepts(Error, negate=True)(dummy_function), ErrorB, True), ], ) def test_returns_bool( self, function: Callable, argument: object, return_value: bool, ) -> None: assert function(argument) is return_value def test_reraises(self) -> None: with pytest.raises(BaseError): excepts(Error)(dummy_function)(BaseError) ================================================ FILE: tests/test_fn.yaml ================================================ - case: test_compose2_can_infer_types disable_cache: true regex: true main: | from typing import Callable from phantom import Predicate from phantom.fn import compose2 from predicates import is_valid_name reveal_type(is_valid_name) is_valid_name(1) is_valid_name([]) reveal_type(is_valid_name("")) def takes_str_predicate(predicate: Predicate[str]) -> None: ... takes_str_predicate(is_valid_name) reveal_type(compose2(int, str)(2)) def takes_int_str_composed(fn: Callable[[int], int]) -> None: ... takes_int_str_composed(compose2(int, str)) reveal_type(compose2(str, int)("3")) def takes_str_int_composed(fn: Callable[[str], str]) -> None: ... takes_str_int_composed(compose2(str, int)) out: | main:6: note: Revealed type is "def \(builtins\.str\*?\) -> builtins\.bool\*?" main:7: error: Argument 1 has incompatible type "int"; expected "str" \[arg-type\] main:8: error: Argument 1 has incompatible type "list\[Never\]"; expected "str" \[arg-type\] main:9: note: Revealed type is "builtins.bool\*?" main:13: note: Revealed type is "builtins.int\*?" main:17: note: Revealed type is "builtins.str\*?" files: - path: predicates.py content: | from __future__ import annotations from functools import partial from typing import Callable from typing import Sequence from phantom import Predicate from phantom.fn import compose2 from phantom.predicates.boolean import both from phantom.predicates.collection import count from phantom.predicates.collection import every from phantom.predicates.generic import equal as_parts: Callable[[str], list[str]] = partial(str.split, sep=".") is_valid_parts: Predicate[Sequence[str]] = both( count(equal(3)), every(str.isidentifier), ) is_valid_name = compose2(is_valid_parts, as_parts) ================================================ FILE: tests/test_intersection.yaml ================================================ - case: asserting_twice_results_in_intersection_type main: | from phantom.interval import Portion, Exclusive class Positive(float, Exclusive, min=0): ... n = .5 assert isinstance(n, Positive) assert isinstance(n, Portion) reveal_type(n) # N: Revealed type is "main." ================================================ FILE: tests/test_interval.py ================================================ from __future__ import annotations from decimal import Decimal from functools import total_ordering import pytest from phantom.interval import Inclusive from phantom.interval import Interval from phantom.interval import Natural from phantom.interval import NegativeInt from phantom.interval import Portion from phantom.interval import _get_scalar_float_bounds from phantom.interval import _get_scalar_int_bounds from phantom.interval import _NonScalarBounds from phantom.predicates import interval from .types import FloatInc from .types import FloatIncExc from .types import IntExc from .types import IntExcInc class TestInterval: def test_subclassing_without_check_raises(self): with pytest.raises(TypeError, match="A must define an interval check$"): class A(Interval, abstract=False): ... def test_parse_coerces_str(self): class Great(int, Inclusive, low=10): ... assert Great.parse("10") == 10 def test_allows_decimal_bound(self): class A( Decimal, Interval, check=interval.exclusive, low=Decimal("1.15"), high=Decimal("2.36"), ): ... assert not isinstance(2, A) assert not isinstance(1.98, A) assert isinstance(Decimal("1.98"), A) def test_subclass_inherits_bounds(self): class A(int, Inclusive, low=-10, high=10): ... class B(A): ... assert B.__check__ is A.__check__ assert isinstance(-10, B) assert isinstance(10, B) assert not isinstance(-11, B) assert not isinstance(11, B) class C(A, low=0): ... assert C.__check__ is A.__check__ assert isinstance(0, C) assert isinstance(10, C) assert not isinstance(-1, C) assert not isinstance(11, C) class D(A, high=0): ... assert D.__check__ is A.__check__ assert isinstance(-10, D) assert isinstance(0, D) assert not isinstance(-11, D) assert not isinstance(1, D) parametrize_negative_ints = pytest.mark.parametrize("i", (-10, -1, -0, +0)) parametrize_positive_ints = pytest.mark.parametrize("i", (10, 1, +0, -0)) class TestNegativeInt: @parametrize_negative_ints def test_negative_int_is_instance(self, i): assert isinstance(i, NegativeInt) def test_positive_int_is_not_instance(self): assert not isinstance(1, NegativeInt) assert not isinstance(10, NegativeInt) def test_instantiation_raises_for_positive_int(self): with pytest.raises(TypeError): NegativeInt.parse(1) with pytest.raises(TypeError): NegativeInt.parse(10) @parametrize_negative_ints def test_instantiation_returns_instance(self, i): assert i is NegativeInt.parse(i) class TestNatural: @parametrize_positive_ints def test_positive_int_is_instance(self, i): assert isinstance(i, Natural) def test_negative_int_is_not_instance(self): assert not isinstance(-1, Natural) assert not isinstance(-10, Natural) def test_instantiation_raises_for_positive_int(self): with pytest.raises(TypeError): Natural.parse(-1) with pytest.raises(TypeError): Natural.parse(-10) with pytest.raises(TypeError): Natural(-1) @parametrize_positive_ints def test_instantiation_returns_instance(self, i): assert i is Natural.parse(i) assert i is Natural(i) parametrize_portion_values = pytest.mark.parametrize( "i", (0.0, 1.0, -0.0, +1.0, 0.8652559794322651) ) # TODO: Use math.nextafter on Python 3.9 to test as close to limit as possible parametrize_non_portion_values = pytest.mark.parametrize("i", (-1, -0.1, 1.1, 2)) class TestPortion: @parametrize_portion_values def test_value_inside_range_is_instance(self, i): assert isinstance(i, Portion) @parametrize_non_portion_values def test_value_outside_range_is_instance(self, i): assert not isinstance(i, Portion) @parametrize_portion_values def test_instantiation_returns_instance(self, i): assert i is Portion.parse(i) assert i is Portion(i) @parametrize_non_portion_values def test_instantiation_raises_for_non_portion_values(self, i): with pytest.raises(TypeError): Portion.parse(i) with pytest.raises(TypeError): Portion(i) class TestGetScalarIntBounds: @pytest.mark.parametrize( ("type_", "exclude_min", "exclude_max", "expected_low", "expected_high"), ( (FloatInc, False, False, 0, 100), (IntExc, True, True, 1, 99), (IntExcInc, True, False, 1, 100), (FloatIncExc, False, True, 0, 99), (Natural, False, False, 0, None), (NegativeInt, False, False, None, 0), ), ) def test_returns_correct_bounds( self, type_: type[Interval], exclude_min: bool, exclude_max: bool, expected_low: int | None, expected_high: int | None, ): (low, high) = _get_scalar_int_bounds(type_, exclude_min, exclude_max) assert low == expected_low assert high == expected_high def test_raises_non_scalar_bounds_for_non_int_lower_bound(self): @total_ordering class Inf: def __eq__(self, other): return False def __lt__(self, other): return False class Int(int, Inclusive, low=Inf(), high=100): ... with pytest.raises(_NonScalarBounds): _get_scalar_int_bounds(Int) def test_raises_non_scalar_bounds_for_non_int_upper_bound(self): @total_ordering class Inf: def __eq__(self, other): return False def __lt__(self, other): return False class Int(int, Inclusive, low=0, high=Inf()): ... with pytest.raises(_NonScalarBounds): _get_scalar_int_bounds(Int) class TestGetScalarFloatBounds: @pytest.mark.parametrize( ("type_", "expected_low", "expected_high"), ( (FloatInc, 0, 100), (Natural, 0, None), (NegativeInt, None, 0), (Portion, 0, 1), ), ) def test_returns_correct_bounds( self, type_: type[Interval], expected_low: int, expected_high: int, ): (low, high) = _get_scalar_float_bounds(type_) assert low == expected_low assert high == expected_high def test_raises_non_scalar_bounds_for_non_int_lower_bound(self): @total_ordering class Inf: def __eq__(self, other): return False def __lt__(self, other): return False class Int(float, Inclusive, low=Inf(), high=100): ... with pytest.raises(_NonScalarBounds): _get_scalar_float_bounds(Int) def test_raises_non_scalar_bounds_for_non_int_upper_bound(self): @total_ordering class Inf: def __eq__(self, other): return False def __lt__(self, other): return False class Int(float, Inclusive, low=0, high=Inf()): ... with pytest.raises(_NonScalarBounds): _get_scalar_float_bounds(Int) ================================================ FILE: tests/test_interval.yaml ================================================ - case: calling_function_with_unknown_raises main: | from phantom.interval import Natural def take_nat(a: Natural) -> Natural: return a take_nat(1) # E: Argument 1 to "take_nat" has incompatible type "int"; expected "Natural" [arg-type] - case: calling_function_with_known_1 main: | from phantom.interval import Natural def take_nat(a: Natural) -> Natural: return a b = take_nat(Natural.parse(1)) reveal_type(b) # N: Revealed type is "phantom.interval.Natural" - case: calling_function_with_known_2 main: | from phantom.interval import Natural def from_int(a: int) -> Natural: if not isinstance(a, Natural): raise TypeError reveal_type(a) # N: Revealed type is "phantom.interval.Natural" return a n = from_int(-1) reveal_type(n) # N: Revealed type is "phantom.interval.Natural" - case: test_overload main: | from typing import overload from phantom.interval import Natural, NegativeInt @overload def add(a: Natural, b: Natural) -> Natural: ... @overload def add(a: NegativeInt, b: NegativeInt) -> NegativeInt: ... @overload def add(a: int, b: int) -> int: ... def add(a: int, b: int) -> int: return a + b a = Natural.parse(1) b = NegativeInt.parse(-1) reveal_type(add(a, a)) # N: Revealed type is "phantom.interval.Natural" reveal_type(add(b, b)) # N: Revealed type is "phantom.interval.NegativeInt" reveal_type(add(a, b)) # N: Revealed type is "builtins.int" reveal_type(add(b, a)) # N: Revealed type is "builtins.int" ================================================ FILE: tests/test_iso3166.py ================================================ import pytest from phantom.iso3166 import InvalidCountryCode from phantom.iso3166 import ParsedAlpha2 from phantom.iso3166 import normalize_alpha2_country_code class TestNormalizeAlpha2CountryCode: @pytest.mark.parametrize( "given, normalized", (("sE", "SE"), ("dk", "DK"), ("IE", "IE")), ) def test_normalizes_mixed_case_valid_country_code( self, given: str, normalized: str ) -> None: assert normalize_alpha2_country_code(given) == normalized @pytest.mark.parametrize("invalid", ("UK", "not a country code")) def test_raises_for_invalid_country_code(self, invalid: str) -> None: with pytest.raises(InvalidCountryCode): normalize_alpha2_country_code(invalid) class TestAlpha2: @pytest.mark.parametrize("invalid", ("SP", "DA", "AV", 1)) def test_invalid_country_code_is_not_instance(self, invalid: object) -> None: assert not isinstance(invalid, ParsedAlpha2) @pytest.mark.parametrize("country_code", ("PS", "AD", "VA")) def test_valid_country_code_is_instance(self, country_code: str) -> None: assert isinstance(country_code, ParsedAlpha2) def test_normalizes_valid_country_code(self) -> None: country_code = ParsedAlpha2.parse("ps") assert country_code == "PS" assert isinstance(country_code, ParsedAlpha2) @pytest.mark.parametrize("invalid", ("SP", "DA", "AV")) def test_raises_for_invalid_country_code(self, invalid: str) -> None: with pytest.raises(InvalidCountryCode): ParsedAlpha2.parse(invalid) ================================================ FILE: tests/test_iso3166.yaml ================================================ - case: can_instantiate main: | from phantom.iso3166 import Alpha2, ParsedAlpha2 def takes_country_code(a: Alpha2) -> None: ... takes_country_code(ParsedAlpha2.parse("")) - case: can_use_literal main: | from phantom.iso3166 import Alpha2 def takes_country_code(a: Alpha2) -> None: ... takes_country_code("FR") - case: can_infer main: | from phantom.iso3166 import ParsedAlpha2, Alpha2 def takes_country_code(a: Alpha2) -> None: ... a = "" assert isinstance(a, ParsedAlpha2) takes_country_code(a) - case: bound_is_not_subtype main: | from phantom.iso3166 import CountryCode def takes_country_code(a: CountryCode) -> None: ... takes_country_code("fr") out: | main:6: error: Argument 1 to "takes_country_code" has incompatible type "Literal['fr']"; expected "Literal['AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ', 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR', 'IO', 'BN', 'BG', 'BF', 'BI', 'KH', 'CM', 'CA', 'CV', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', 'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO', 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF', 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', 'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'XK', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', 'LI', 'LT', 'LU', 'MO', 'MK', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX', 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI', 'NE', 'NG', 'NU', 'NF', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', 'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', 'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', 'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SZ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK', 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW'] | ParsedAlpha2" [arg-type] ================================================ FILE: tests/test_negated.py ================================================ from typing import get_args from typing import get_origin import pytest from phantom.negated import SequenceNotStr parametrize_instances = pytest.mark.parametrize( "value", ( ("foo", "bar", "baz"), (1, 2, object()), (b"hello", b"there"), [], ["foo"], [b"bar"], ), ) parametrize_non_instances = pytest.mark.parametrize( "value", ( "", "foo", object(), b"", b"foo", {}, set(), frozenset(), ), ) class TestSequenceNotStr: @parametrize_instances def test_is_instance(self, value: object): assert isinstance(value, SequenceNotStr) @parametrize_non_instances def test_is_not_instance(self, value: object): assert not isinstance(value, SequenceNotStr) @parametrize_instances def test_parse_returns_instance(self, value: object): assert SequenceNotStr.parse(value) is value @parametrize_non_instances def test_parse_raises_for_non_instances(self, value: object): with pytest.raises(TypeError): SequenceNotStr.parse(value) def test_subscription_returns_type_alias(self): alias = SequenceNotStr[str] assert get_origin(alias) is SequenceNotStr (arg,) = get_args(alias) assert arg is str ================================================ FILE: tests/test_negated.yaml ================================================ - case: test_subscripted main: | from phantom.negated import SequenceNotStr def greet(names: SequenceNotStr[str]) -> str: return f"Hello {', '.join(names)}" seq = SequenceNotStr[str].parse(("Jane", "Joe")) greeting = greet(seq) reveal_type(greeting) # N: Revealed type is "builtins.str" - case: test_annotated main: | from phantom.negated import SequenceNotStr def greet(names: SequenceNotStr[str]) -> str: return f"Hello {', '.join(names)}" seq: SequenceNotStr[str] = SequenceNotStr.parse(("Jane", "Joe")) greeting = greet(seq) reveal_type(greeting) # N: Revealed type is "builtins.str" - case: test_unspecified_inner_type main: | from phantom.negated import SequenceNotStr from typing import Tuple def fn(values: SequenceNotStr) -> SequenceNotStr[object]: return values seq: Tuple[object, ...] = (1, 2, object()) assert isinstance(seq, SequenceNotStr) reveal_type(seq) # N: Revealed type is "main." values = fn(seq) reveal_type(values) # N: Revealed type is "phantom.negated.SequenceNotStr[builtins.object]" ================================================ FILE: tests/test_re.py ================================================ from __future__ import annotations import re import pytest from phantom.re import FullMatch from phantom.re import Match class MatchPatternInstance(Match, pattern=re.compile(r"abc")): ... class MatchPatternStr(Match, pattern=r"abc"): ... parametrize_match = pytest.mark.parametrize( "match_type", (MatchPatternInstance, MatchPatternStr) ) class TestMatch: @parametrize_match def test_non_matching_string_is_not_instance(self, match_type: type[Match]): assert not isinstance("abd", match_type) @parametrize_match def test_matching_string_is_instance(self, match_type: type[Match]): assert isinstance("abcd", match_type) @parametrize_match def test_instantiation_raises_for_non_matching_string( self, match_type: type[Match] ): with pytest.raises(TypeError): match_type.parse("b") @parametrize_match def test_instantiation_returns_instance(self, match_type: type[Match]): s = "abc" assert s is match_type.parse(s) class FullMatchPatternInstance(FullMatch, pattern=re.compile(r"abc")): ... class FullMatchStr(FullMatch, pattern=r"abc"): ... parametrize_full_match = pytest.mark.parametrize( "full_match_type", (FullMatchPatternInstance, FullMatchStr) ) class TestFullMatch: @parametrize_full_match def test_non_matching_string_is_not_instance( self, full_match_type: type[FullMatch] ): assert not isinstance("abcd", full_match_type) @parametrize_full_match def test_matching_string_is_instance(self, full_match_type: type[FullMatch]): assert isinstance("abc", full_match_type) @parametrize_full_match def test_instantiation_raises_for_non_matching_string( self, full_match_type: type[FullMatch] ): with pytest.raises(TypeError): full_match_type.parse("b") @parametrize_full_match def test_instantiation_returns_instance(self, full_match_type: type[FullMatch]): s = "abc" assert s is full_match_type.parse(s) ================================================ FILE: tests/test_re.yaml ================================================ - case: str_is_not_subtype_of_match main: | import re from phantom.re import Match class A(Match, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a take_a("c") # E: Argument 1 to "take_a" has incompatible type "str"; expected "A" [arg-type] - case: str_is_not_subtype_of_full_match main: | import re from phantom.re import FullMatch class A(FullMatch, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a take_a("c") # E: Argument 1 to "take_a" has incompatible type "str"; expected "A" [arg-type] - case: can_instantiate_match main: | import re from phantom.re import Match class A(Match, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a a = take_a(A.parse("abc")) reveal_type(a) # N: Revealed type is "main.A" - case: can_instantiate_full_match main: | import re from phantom.re import FullMatch class A(FullMatch, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a a = take_a(A.parse("abc")) reveal_type(a) # N: Revealed type is "main.A" - case: can_infer_match main: | import re from phantom.re import Match class A(Match, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a a = "abc" assert isinstance(a, A) a = take_a(a) - case: can_infer_full_match main: | import re from phantom.re import FullMatch class A(FullMatch, pattern=re.compile(r"^abc$")): ... def take_a(a: A) -> A: return a a = "abc" assert isinstance(a, A) a = take_a(a) ================================================ FILE: tests/test_sized.py ================================================ from dataclasses import dataclass from typing import Final from typing import Generic from typing import TypeVar from typing import get_args from typing import get_origin import pytest from phantom.predicates.numeric import odd from phantom.sized import Empty from phantom.sized import LSPViolation from phantom.sized import NonEmpty from phantom.sized import NonEmptyStr from phantom.sized import PhantomBound from phantom.sized import PhantomSized from phantom.sized import UnresolvedBounds @dataclass class MutableDataclass: value: str = "foo" parametrize_non_empty: Final = pytest.mark.parametrize( "container", ((1,), frozenset({1}), "foo"), ) parametrize_empty: Final = pytest.mark.parametrize( "container", ((), frozenset(), ""), ) parametrize_mutable: Final = pytest.mark.parametrize( "container", ([], [1], set(), {1}, {}, {1: 2}, MutableDataclass()), ) T = TypeVar("T") class OddSize(PhantomSized[T], Generic[T], len=odd): ... class TestPhantomSized: odd_length = pytest.mark.parametrize( "container", [ (1,), (1, 2, 3), (1, 2, 3, 4, 5), frozenset({1}), frozenset({1, 2, 3, 4, 5}), ], ) even_length = pytest.mark.parametrize( "container", [ (), (1, 1), (1, 1, 1, 2, 3, 6), frozenset(), frozenset({1, 2}), ], ) @odd_length def test_value_fulfilling_predicate_is_instance(self, container: object): assert isinstance(container, OddSize) @even_length def test_value_not_fulfilling_predicate_is_not_instance(self, container: object): assert not isinstance(container, OddSize) @even_length def test_instantiation_raises_for_invalid_length(self, container: object): with pytest.raises(TypeError): OddSize.parse(container) @parametrize_mutable def test_instantiation_raises_for_mutable(self, container: object): with pytest.raises(TypeError): OddSize.parse(container) @parametrize_non_empty def test_instantiation_returns_instance(self, container: object): assert container is OddSize.parse(container) def test_subscription_returns_type_alias(self): alias = OddSize[tuple] assert get_origin(alias) is OddSize (arg,) = get_args(alias) assert arg is tuple class Tens(PhantomBound[T], Generic[T], min=10, max=19): ... class TestPhantomBound: valid = pytest.mark.parametrize( "container", [10 * (1,), 19 * (2,)], ) invalid = pytest.mark.parametrize( "container", [9 * (1,), 20 * (2,)], ) @valid def test_value_fulfilling_predicate_is_instance(self, container: object): assert isinstance(container, Tens) @invalid def test_value_not_fulfilling_predicate_is_not_instance(self, container: object): assert not isinstance(container, Tens) @invalid def test_instantiation_raises_for_invalid_length(self, container: object): with pytest.raises(TypeError): Tens.parse(container) @parametrize_mutable def test_instantiation_raises_for_mutable(self, container: object): with pytest.raises(TypeError): Tens.parse(container) @valid def test_instantiation_returns_instance(self, container: object): assert container is Tens.parse(container) def test_subscription_returns_type_alias(self): alias = Tens[tuple] assert get_origin(alias) is Tens (arg,) = get_args(alias) assert arg is tuple def test_raises_lsp_violation_when_attempting_to_decrease_min(self): with pytest.raises(LSPViolation): class Lower(Tens, min=9): ... def test_raises_lsp_violation_when_attempting_to_increase_max(self): with pytest.raises(LSPViolation): class Higher(Tens, max=20): ... def test_can_narrow_range_in_subclass(self): class Fewer(Tens, min=11, max=18): ... assert isinstance(11 * (0,), Fewer) assert isinstance(18 * (0,), Fewer) assert not isinstance(10 * (0,), Fewer) assert not isinstance(19 * (0,), Fewer) def test_abstract_subclass_can_omit_bounds(self): class A(PhantomBound, abstract=True): ... class B(A, min=10, max=20): ... assert B.__min__ == 10 assert B.__max__ == 20 def test_raises_unresolved_bounds_when_concrete_subclass_omits_bounds(self): with pytest.raises(UnresolvedBounds): class A(PhantomBound): ... class TestNonEmpty: @parametrize_non_empty def test_non_empty_container_is_instance(self, container): assert isinstance(container, NonEmpty) @parametrize_empty def test_empty_container_is_instance(self, container): assert not isinstance(container, NonEmpty) @parametrize_empty def test_instantiation_raises_for_empty_container(self, container): with pytest.raises(TypeError): NonEmpty.parse(container) @parametrize_mutable def test_instantiation_raises_for_mutable(self, container): with pytest.raises(TypeError): NonEmpty.parse(container) @parametrize_non_empty def test_instantiation_returns_instance(self, container): assert container is NonEmpty.parse(container) def test_subscription_returns_type_alias(self): alias = NonEmpty[tuple] assert get_origin(alias) is NonEmpty (arg,) = get_args(alias) assert arg is tuple class TestEmpty: @parametrize_non_empty def test_non_empty_container_is_instance(self, container): assert not isinstance(container, Empty) @parametrize_empty def test_empty_container_is_instance(self, container): assert isinstance(container, Empty) @parametrize_non_empty def test_instantiation_raises_for_non_empty_container(self, container): with pytest.raises(TypeError): Empty.parse(container) @parametrize_mutable def test_instantiation_raises_for_mutable(self, container): with pytest.raises(TypeError): Empty.parse(container) @parametrize_empty def test_instantiation_returns_instance(self, container): assert container is Empty.parse(container) def test_subscription_returns_type_alias(self): alias = Empty[frozenset] assert get_origin(alias) is Empty (arg,) = get_args(alias) assert arg is frozenset parametrize_non_empty_strs: Final = pytest.mark.parametrize( "value", ("foo", "bar", " "), ) class TestNonEmptyStr: @parametrize_non_empty_strs def test_non_empty_str_is_instance(self, value: str): assert isinstance(value, NonEmptyStr) def test_empty_str_is_not_instance(self): assert not isinstance("", NonEmptyStr) def test_instantiation_raises_for_empty_str(self): with pytest.raises(TypeError): NonEmptyStr.parse("") @pytest.mark.parametrize( "value", (b"", b"foo", [], ["foo"], (), ("foo",)), ) def test_instantiation_raises_for_non_str(self, value: object): with pytest.raises(TypeError): NonEmptyStr.parse(value) @parametrize_non_empty_strs def test_instantiation_returns_instance(self, value: str): assert value is NonEmptyStr.parse(value) ================================================ FILE: tests/test_sized.yaml ================================================ - case: test_subtype main: | from phantom.sized import NonEmpty from typing import Tuple class AtLeastOneInt(Tuple[int, ...], NonEmpty[int]): ... def fst(things: AtLeastOneInt) -> int: return things[0] f = fst(AtLeastOneInt.parse((1, 2))) reveal_type(f) # N: Revealed type is "builtins.int" - case: test_subscripted main: | from phantom.sized import NonEmpty def fst(col: NonEmpty[int]) -> int: return next(iter(col)) l = NonEmpty[int].parse((0,)) i = fst(l) reveal_type(i) # N: Revealed type is "builtins.int" - case: test_annotated main: | from phantom.sized import NonEmpty def fst(col: NonEmpty[int]) -> int: return next(iter(col)) l: NonEmpty[int] = NonEmpty.parse((0,)) i = fst(l) reveal_type(i) # N: Revealed type is "builtins.int" - case: test_str main: | from phantom.sized import NonEmpty def name_length(name: NonEmpty[str]) -> int: return len(name) i = name_length(NonEmpty.parse("foo")) reveal_type(i) # N: Revealed type is "builtins.int" ================================================ FILE: tests/test_utils.py ================================================ from typing import Union import pytest from phantom._utils.misc import BoundType from phantom._utils.misc import is_subtype class A: ... class B: ... class C: ... class AAndB(A, B): ... class SubOfA(A): ... class TestIsSubtype: # The cases are annotated with the numbered cases in the docstring of the function. @pytest.mark.parametrize( "a, b", [ # 1 (int | float, int | float), # 2 (int, int | float), # Special case of 3 (Union[int], int), # noqa: UP007 # 4 (AAndB, (A, B)), (AAndB, (A,)), # 5 ((B, SubOfA), A), # 6 ((SubOfA, C), A | B), # Special case of 7 (Union[int], (int,)), # noqa: UP007 # 8 ((AAndB, SubOfA), (A, B)), # 9 (SubOfA, A), ], ) def test_returns_true_for_valid_subtype(self, a: BoundType, b: BoundType) -> None: assert is_subtype(a, b) is True # The cases are annotated with the numbered cases in the docstring of the function. @pytest.mark.parametrize( "a, b", [ # 1 (int | float, Union[int]), # noqa: UP007 (int | float, A | B), # 2 (str, int | float), # 3 (int | float, float), # 4 (SubOfA, (A, B)), (SubOfA, (B,)), # 5 ((A, B), SubOfA), # 6 ((int, C), A | B), # 7 (int | float, (int, float)), # 8 ((AAndB, SubOfA), (A, B, C)), # 9 (SubOfA, B), (A, B), ], ) def test_returns_false_for_valid_subtype(self, a: BoundType, b: BoundType) -> None: assert is_subtype(a, b) is False ================================================ FILE: tests/types.py ================================================ from phantom.interval import Exclusive from phantom.interval import ExclusiveInclusive from phantom.interval import Inclusive from phantom.interval import InclusiveExclusive class FloatInc(float, Inclusive, low=0, high=100): ... class IntInc(int, Inclusive, low=0, high=100): ... class FloatExc(float, Exclusive, low=0, high=100): ... class IntExc(int, Exclusive, low=0, high=100): ... class FloatIncExc(float, InclusiveExclusive, low=0, high=100): ... class IntIncExc(int, InclusiveExclusive, low=0, high=100): ... class FloatExcInc(float, ExclusiveInclusive, low=0, high=100): ... class IntExcInc(int, ExclusiveInclusive, low=0, high=100): ... ================================================ FILE: typing-requirements.txt ================================================ # This file was autogenerated by uv via the following command: # 'make typing-requirements' beartype==0.22.5 \ --hash=sha256:516a9096cc77103c96153474fa35c3ebcd9d36bd2ec8d0e3a43307ced0fa6341 \ --hash=sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0 # via numerary hypothesis==6.147.0 \ --hash=sha256:72e6004ea3bd1460bdb4640b6389df23b87ba7a4851893fd84d1375635d3e507 \ --hash=sha256:de588807b6da33550d32f47bcd42b1a86d061df85673aa73e6443680249d185e # via phantom-types (pyproject.toml) iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest mypy==1.18.2 \ --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 # via phantom-types (pyproject.toml) mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy numerary==0.4.4 \ --hash=sha256:ad955ddf7f5275f8e52f5520b2d6c654cc3bf1e3ae4bfb45664c9d51b208d0c6 # via phantom-types (pyproject.toml) packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via pytest pathspec==0.12.1 \ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 # via mypy phonenumbers==9.0.18 \ --hash=sha256:5537c61ba95b11b992c95e804da6e49193cc06b1224f632ade64631518a48ed1 \ --hash=sha256:d3354454ac31c97f8a08121df97a7145b8dca641f734c6f1518a41c2f60c5764 # via phantom-types (pyproject.toml) pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest pydantic==1.10.24 \ --hash=sha256:02f7a25e8949d8ca568e4bcef2ffed7881d7843286e7c3488bdd3b67f092059c \ --hash=sha256:076fff9da02ca716e4c8299c68512fdfbeac32fdefc9c160e6f80bdadca0993d \ --hash=sha256:093768eba26db55a88b12f3073017e3fdee319ef60d3aef5c6c04a4e484db193 \ --hash=sha256:0cbbf306124ae41cc153fdc2559b37faa1bec9a23ef7b082c1756d1315ceffe6 \ --hash=sha256:17e7610119483f03954569c18d4de16f4e92f1585f20975414033ac2d4a96624 \ --hash=sha256:1a1ae996daa3d43c530b8d0bacc7e2d9cb55e3991f0e6b7cc2cb61a0fb9f6667 \ --hash=sha256:25fb9a69a21d711deb5acefdab9ff8fb49e6cc77fdd46d38217d433bff2e3de2 \ --hash=sha256:265788a1120285c4955f8b3d52b3ea6a52c7a74db097c4c13a4d3567f0c6df3c \ --hash=sha256:2d1a5ef77efeb54def2695f2b8f4301aae8c7aa2b334bd15f61c18ef54317621 \ --hash=sha256:34109b0afa63b36eec2f2b115694e48ae5ee52f7d3c1baa0be36f80e586bda52 \ --hash=sha256:415c638ca5fd57b915a62dd38c18c8e0afe5adf5527be6f8ce16b4636b616816 \ --hash=sha256:49a6f0178063f15eaea6cbcb2dba04db0b73db9834bc7b1e1c4dbea28c7cd22f \ --hash=sha256:4a9e92b9c78d7f3cfa085c21c110e7000894446e24a836d006aabfc6ae3f1813 \ --hash=sha256:4d7336bfcdb8cb58411e6b498772ba2cff84a2ce92f389bae3a8f1bb2c840c49 \ --hash=sha256:50d9f8a207c07f347d4b34806dc576872000d9a60fd481ed9eb78ea8512e0666 \ --hash=sha256:52219b4e70c1db185cfd103a804e416384e1c8950168a2d4f385664c7c35d21a \ --hash=sha256:58d42a7c344882c00e3bb7c6c8c6f62db2e3aafa671f307271c45ad96e8ccf7a \ --hash=sha256:5a42033fac69b9f1f867ecc3a2159f0e94dceb1abfc509ad57e9e88d49774683 \ --hash=sha256:5ce0986799248082e9a5a026c9b5d2f9fa2e24d2afb9b0eace9104334a58fdc1 \ --hash=sha256:5da2775712dda8b89e701ed2a72d5d81d23dbc6af84089da8a0f61a0be439c8c \ --hash=sha256:5fc35569dfd15d3b3fc06a22abee0a45fdde0784be644e650a8769cd0b2abd94 \ --hash=sha256:6af36a8fb3072526b5b38d3f341b12d8f423188e7d185f130c0079fe02cdec7f \ --hash=sha256:6f25d2f792afcd874cc8339c1da1cc52739f4f3d52993ed1f6c263ef2afadc47 \ --hash=sha256:70152291488f8d2bbcf2027b5c28c27724c78a7949c91b466d28ad75d6d12702 \ --hash=sha256:75259be0558ca3af09192ad7b18557f2e9033ad4cbd48c252131f5292f6374fd \ --hash=sha256:7c8bbad6037a87effe9f3739bdf39851add6e0f7e101d103a601c504892ffa70 \ --hash=sha256:7e6d1af1bd3d2312079f28c9baf2aafb4a452a06b50717526e5ac562e37baa53 \ --hash=sha256:8057172868b0d98f95e6fcddcc5f75d01570e85c6308702dd2c50ea673bc197b \ --hash=sha256:82f951210ebcdb778b1d93075af43adcd04e9ebfd4f44b1baa8eeb21fbd71e36 \ --hash=sha256:874a78e4ed821258295a472e325eee7de3d91ba7a61d0639ce1b0367a3c63d4c \ --hash=sha256:8f2447ca88a7e14fd4d268857521fb37535c53a367b594fa2d7c2551af905993 \ --hash=sha256:956b30638272c51c85caaff76851b60db4b339022c0ee6eca677c41e3646255b \ --hash=sha256:9c377fc30d9ca40dbff5fd79c5a5e1f0d6fff040fa47a18851bb6b0bd040a5d8 \ --hash=sha256:a5bf94042efbc6ab56b18a5921f426ebbeefc04f554a911d76029e7be9057d01 \ --hash=sha256:af31565b12a7db5bfa5fe8c3a4f8fda4d32f5c2929998b1b241f1c22e9ab6e69 \ --hash=sha256:af8e2b3648128b8cadb1a71e2f8092a6f42d4ca123fad7a8d7ce6db8938b1db3 \ --hash=sha256:b644d6f14b2ce617d6def21622f9ba73961a16b7dffdba7f6692e2f66fa05d00 \ --hash=sha256:b66e4892d8ae005f436a5c5f1519ecf837574d8414b1c93860fb3c13943d9b37 \ --hash=sha256:bb3df10be3c7d264947180615819aeec0916f19650f2ba7309ed1fe546ead0d2 \ --hash=sha256:bed9d6eea5fabbc6978c42e947190c7bd628ddaff3b56fc963fe696c3710ccd6 \ --hash=sha256:c626596c1b95dc6d45f7129f10b6743fbb50f29d942d25a22b2ceead670c067d \ --hash=sha256:d255bebd927e5f1e026b32605684f7b6fc36a13e62b07cb97b29027b91657def \ --hash=sha256:d6e45dbc79a44e34c2c83ef1fcb56ff663040474dcf4dfc452db24a1de0f7574 \ --hash=sha256:e24435a9970dcb2b35648f2cf57505d4bd414fcca1a404c82e28d948183fe0a6 \ --hash=sha256:eef07ea2fba12f9188cfa2c50cb3eaa6516b56c33e2a8cc3cd288b4190ee6c0c \ --hash=sha256:ef14dfa7c98b314a3e449e92df6f1479cafe74c626952f353ff0176b075070de \ --hash=sha256:f154a8a46a0d950c055254f8f010ba07e742ac4404a3b6e281a31913ac45ccd0 \ --hash=sha256:fa0ebefc169439267e4b4147c7d458908788367640509ed32c90a91a63ebb579 \ --hash=sha256:fac7fbcb65171959973f3136d0792c3d1668bc01fd414738f0898b01f692f1b4 \ --hash=sha256:fc3f4a6544517380658b63b144c7d43d5276a343012913b7e5d18d9fba2f12bb # via phantom-types (pyproject.toml) pygments==2.19.2 \ --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b # via pytest pytest==9.0.0 \ --hash=sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e \ --hash=sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96 # via phantom-types (pyproject.toml) python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via phantom-types (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via hypothesis typeguard==4.4.4 \ --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e # via phantom-types (pyproject.toml) types-python-dateutil==2.9.0.20251108 \ --hash=sha256:a4a537f0ea7126f8ccc2763eec9aa31ac8609e3c8e530eb2ddc5ee234b3cd764 \ --hash=sha256:d8a6687e197f2fa71779ce36176c666841f811368710ab8d274b876424ebfcaa # via phantom-types (pyproject.toml) typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # phantom-types (pyproject.toml) # mypy # pydantic # typeguard