Showing preview only (318K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<p align=center><img src=https://raw.githubusercontent.com/antonagestam/phantom-types/main/docs/phantom.svg></p>
<h1 align=center>phantom-types</h1>
<p align=center>
<a href=https://github.com/antonagestam/phantom-types/actions?query=workflow%3ACI+branch%3Amain><img src=https://github.com/antonagestam/phantom-types/actions/workflows/ci.yaml/badge.svg?branch=main alt="CI Build Status"></a>
<a href=https://phantom-types.readthedocs.io/en/stable/><img src=https://readthedocs.org/projects/phantom-types/badge/?version=main alt="Documentation Build Status"></a>
<a href=https://codecov.io/gh/antonagestam/phantom-types><img src=https://codecov.io/gh/antonagestam/phantom-types/branch/main/graph/badge.svg?token=UE85B7IA3Q alt="Test coverage report"></a>
<br>
<a href=https://pypi.org/project/phantom-types/><img src=https://img.shields.io/pypi/v/phantom-types.svg?color=informational&label=PyPI alt="PyPI Package"></a>
<a href=https://pypi.org/project/phantom-types/><img src=https://img.shields.io/pypi/pyversions/phantom-types.svg?color=informational&label=Python alt="Python versions"></a>
</p>
[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].
<h4 align=center>
<a href=https://phantom-types.readthedocs.io/en/stable/>Checkout the complete documentation on Read the Docs →</a>
</h4>
## 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 <numeric-intervals>`.
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 <phantom.Phantom>`, a check is made that raises
:py:class:`MutableType <phantom.utils.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
<phantom.fn.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 <phantom.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 <types>` that is shipped with the library.
- Check out the basis of :ref:`predicates and predicate<predicates>`
factories to build phantom types from.
- Read more in-depth about :ref:`composing phantom types <composing>`.
================================================
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 <https://github.com/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__() <phantom.Phantom.__get_validators__>` hook
on the base :class:`Phantom <phantom.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__() <phantom.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 <phantom.schema.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__() <phantom.Phantom.__schema__>`. Override
:func:`__schema__() <phantom.Phantom.__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": c
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
SYMBOL INDEX (614 symbols across 48 files)
FILE: docs/conf.py
function get_copyright_from_license (line 18) | def get_copyright_from_license() -> str:
FILE: src/phantom/_base.py
class InstanceCheckable (line 31) | class InstanceCheckable(Protocol):
method __instancecheck__ (line 34) | def __instancecheck__(cls, instance: object) -> bool: ...
class SupportsParse (line 37) | class SupportsParse(Protocol):
method parse (line 40) | def parse(cls, instance: object) -> Self: ...
class PhantomMeta (line 46) | class PhantomMeta(abc.ABCMeta):
method __instancecheck__ (line 52) | def __instancecheck__(self, instance: object) -> bool:
method __call__ (line 57) | def __call__(cls: type[V], instance: object) -> V:
class PhantomBase (line 68) | class PhantomBase(SchemaField, metaclass=PhantomMeta):
method parse (line 70) | def parse(cls: type[Derived], instance: object) -> Derived:
method __instancecheck__ (line 84) | def __instancecheck__(cls, instance: object) -> bool: ...
method __get_validators__ (line 87) | def __get_validators__(cls: type[Derived]) -> Iterator[Callable[[objec...
class AbstractInstanceCheck (line 92) | class AbstractInstanceCheck(TypeError): ...
class MutableType (line 95) | class MutableType(TypeError): ...
class Phantom (line 98) | class Phantom(PhantomBase, Generic[T]):
method __init_subclass__ (line 128) | def __init_subclass__(
method _interpret_implicit_bound (line 146) | def _interpret_implicit_bound(cls) -> BoundType:
method _resolve_bound (line 163) | def _resolve_bound(cls, class_arg: Any) -> None:
method __instancecheck__ (line 192) | def __instancecheck__(cls, instance: object) -> bool:
method __register_strategy__ (line 205) | def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy | None:
FILE: src/phantom/_utils/misc.py
class UnresolvedClassAttribute (line 20) | class UnresolvedClassAttribute(NotImplementedError): ...
function resolve_class_attr (line 23) | def resolve_class_attr(
function _is_union (line 42) | def _is_union(type_: BoundType) -> bool:
function _is_intersection (line 46) | def _is_intersection(type_: BoundType) -> bool:
function is_subtype (line 50) | def is_subtype(a: BoundType, b: BoundType) -> bool: # noqa: C901
function fully_qualified_name (line 109) | def fully_qualified_name(cls: type) -> str:
function is_not_known_mutable_type (line 122) | def is_not_known_mutable_type(type_: BoundType) -> TypeGuard[NotKnownMut...
function is_not_known_mutable_instance (line 131) | def is_not_known_mutable_instance(value: object) -> bool:
function is_union (line 142) | def is_union(value: object) -> TypeGuard[type]:
FILE: src/phantom/_utils/types.py
class _SupportsLt (line 12) | class _SupportsLt(Protocol[T_contra]):
method __lt__ (line 13) | def __lt__(self, other: T_contra) -> bool: ...
class SupportsLt (line 16) | class SupportsLt(
class _SupportsLe (line 24) | class _SupportsLe(Protocol[T_contra]):
method __le__ (line 25) | def __le__(self, other: T_contra) -> bool: ...
class SupportsLe (line 28) | class SupportsLe(
class _SupportsGt (line 36) | class _SupportsGt(Protocol[T_contra]):
method __gt__ (line 37) | def __gt__(self, other: T_contra) -> bool: ...
class SupportsGt (line 40) | class SupportsGt(
class _SupportsGe (line 48) | class _SupportsGe(Protocol[T_contra]):
method __ge__ (line 49) | def __ge__(self, other: T_contra) -> bool: ...
class SupportsGe (line 52) | class SupportsGe(
class _SupportsEq (line 60) | class _SupportsEq(Protocol):
method __eq__ (line 61) | def __eq__(self, other: object) -> bool: ...
method __hash__ (line 62) | def __hash__(self) -> int: ...
class SupportsEq (line 65) | class SupportsEq(
class _Comparable (line 73) | class _Comparable(
class Comparable (line 83) | class Comparable(
class _SupportsFloat (line 91) | class _SupportsFloat(Protocol):
method __float__ (line 92) | def __float__(self) -> float: ...
class SupportsFloat (line 95) | class SupportsFloat(_SupportsFloat, Protocol, metaclass=CachingProtocolM...
class _FloatComparable (line 99) | class _FloatComparable(
class FloatComparable (line 106) | class FloatComparable(
class _SupportsLeGe (line 114) | class _SupportsLeGe(SupportsLe[T_contra], SupportsGe[T_contra], Protocol...
class SupportsLeGe (line 117) | class SupportsLeGe(
class _SupportsLeGt (line 125) | class _SupportsLeGt(SupportsLe[T_contra], SupportsGt[T_contra], Protocol...
class SupportsLeGt (line 128) | class SupportsLeGt(
class _SupportsLtGe (line 136) | class _SupportsLtGe(SupportsLt[T_contra], SupportsGe[T_contra], Protocol...
class SupportsLtGe (line 139) | class SupportsLtGe(
class _SupportsLtGt (line 146) | class _SupportsLtGt(SupportsLt[T_contra], SupportsGt[T_contra], Protocol...
class SupportsLtGt (line 149) | class SupportsLtGt(
class _SupportsMod (line 157) | class _SupportsMod(Protocol[T_contra, U_co]):
method __mod__ (line 158) | def __mod__(self, other: T_contra) -> U_co: ...
class SupportsMod (line 161) | class SupportsMod(
FILE: src/phantom/boolean.py
class Truthy (line 17) | class Truthy(Phantom[object], predicate=boolean.truthy, bound=object):
method __register_strategy__ (line 26) | def __register_strategy__(cls) -> SearchStrategy:
class Falsy (line 41) | class Falsy(Phantom[object], predicate=boolean.falsy, bound=object):
method __register_strategy__ (line 50) | def __register_strategy__(cls) -> SearchStrategy:
FILE: src/phantom/bounds.py
function display_bound (line 25) | def display_bound(bound: Any) -> str:
function get_bound_parser (line 37) | def get_bound_parser(bound: type[T] | Any) -> Parser[T]:
FILE: src/phantom/datetime.py
function parse_datetime_str (line 31) | def parse_datetime_str(
class DateutilParseError (line 40) | class DateutilParseError(Exception): # type: ignore[no-redef]
function parse_datetime (line 47) | def parse_datetime(value: object) -> datetime.datetime:
class TZAware (line 57) | class TZAware(datetime.datetime, Phantom, predicate=is_tz_aware):
method parse (line 72) | def parse(cls, instance: object) -> TZAware:
method __schema__ (line 76) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 83) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy:
class TZNaive (line 90) | class TZNaive(datetime.datetime, Phantom, predicate=is_tz_naive):
method parse (line 99) | def parse(cls, instance: object) -> TZNaive:
method __schema__ (line 103) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 111) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy:
FILE: src/phantom/errors.py
class BoundError (line 1) | class BoundError(TypeError): ...
class MissingDependency (line 4) | class MissingDependency(Exception): ...
FILE: src/phantom/ext/phonenumbers.py
class InvalidPhoneNumber (line 34) | class InvalidPhoneNumber(phonenumbers.NumberParseException, TypeError):
method __init__ (line 37) | def __init__(self, error_type: int = INVALID, msg: str = "Invalid numb...
function _deconstruct_phone_number (line 41) | def _deconstruct_phone_number(
function normalize_phone_number (line 53) | def normalize_phone_number(
function is_formatted_phone_number (line 72) | def is_formatted_phone_number(number: str) -> TypeGuard[FormattedPhoneNu...
class PhoneNumber (line 79) | class PhoneNumber(str, Phantom, predicate=is_phone_number):
method __schema__ (line 81) | def __schema__(cls) -> Schema:
class FormattedPhoneNumber (line 90) | class FormattedPhoneNumber(PhoneNumber, predicate=is_formatted_phone_num...
method parse (line 92) | def parse(cls, instance: object) -> FormattedPhoneNumber:
method __schema__ (line 101) | def __schema__(cls) -> Schema:
FILE: src/phantom/fn.py
function _name (line 10) | def _name(fn: Callable) -> str:
function compose2 (line 24) | def compose2(a: Callable[[AA], AR], b: Callable[[BA], AA]) -> Callable[[...
function excepts (line 46) | def excepts(
FILE: src/phantom/interval.py
class IntervalCheck (line 45) | class IntervalCheck(Protocol):
method __call__ (line 46) | def __call__(self, a: N, b: N) -> Predicate[N]: ...
class _NonScalarBounds (line 53) | class _NonScalarBounds(Exception): ...
function _get_scalar_int_bounds (line 56) | def _get_scalar_int_bounds(
function _get_scalar_float_bounds (line 89) | def _get_scalar_float_bounds(
function _resolve_bound (line 110) | def _resolve_bound(
class Interval (line 128) | class Interval(Phantom[Comparable], bound=Comparable, abstract=True):
method __init_subclass__ (line 143) | def __init_subclass__(
method parse (line 161) | def parse(cls: type[Derived], instance: object) -> Derived:
function _format_limit (line 167) | def _format_limit(value: SupportsEq) -> str:
class Exclusive (line 175) | class Exclusive(Interval, check=interval.exclusive, abstract=True):
method __schema__ (line 179) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 191) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
class Inclusive (line 207) | class Inclusive(Interval, check=interval.inclusive, abstract=True):
method __schema__ (line 211) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 223) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
class ExclusiveInclusive (line 235) | class ExclusiveInclusive(Interval, check=interval.exclusive_inclusive, a...
method __schema__ (line 239) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 251) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
class InclusiveExclusive (line 263) | class InclusiveExclusive(Interval, check=interval.inclusive_exclusive, a...
method __schema__ (line 267) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 279) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
class Natural (line 291) | class Natural(int, InclusiveExclusive, low=0):
method __schema__ (line 295) | def __schema__(cls) -> Schema:
class NegativeInt (line 302) | class NegativeInt(int, ExclusiveInclusive, high=0):
method __schema__ (line 306) | def __schema__(cls) -> Schema:
class Portion (line 313) | class Portion(float, Inclusive, low=0, high=1):
method __schema__ (line 317) | def __schema__(cls) -> Schema:
FILE: src/phantom/iso3166.py
class InvalidCountryCode (line 294) | class InvalidCountryCode(TypeError): ...
function normalize_alpha2_country_code (line 297) | def normalize_alpha2_country_code(country_code: str) -> ParsedAlpha2:
class ParsedAlpha2 (line 309) | class ParsedAlpha2(str, Phantom, predicate=is_alpha2_country_code):
method parse (line 311) | def parse(cls, instance: object) -> ParsedAlpha2:
method __schema__ (line 320) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 330) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
FILE: src/phantom/negated.py
class SequenceNotStr (line 26) | class SequenceNotStr(
method __register_strategy__ (line 36) | def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy:
FILE: src/phantom/predicates/_utils.py
function _explode_partial (line 6) | def _explode_partial(obj: partial) -> str:
function _name_or_repr (line 15) | def _name_or_repr(obj: object) -> str:
function bind_name (line 27) | def bind_name(wrapped: Callable, *values: object) -> Callable[[B], B]:
FILE: src/phantom/predicates/boolean.py
function true (line 11) | def true(_value: object) -> Literal[True]:
function false (line 16) | def false(_value: object) -> Literal[False]:
function negate (line 21) | def negate(predicate: Predicate[T_contra]) -> Predicate[T_contra]:
function truthy (line 31) | def truthy(value: object) -> bool:
function falsy (line 36) | def falsy(value: object) -> bool:
function both (line 41) | def both(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[T_...
function either (line 53) | def either(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[...
function xor (line 66) | def xor(p: Predicate[T_contra], q: Predicate[T_contra]) -> Predicate[T_c...
function all_of (line 79) | def all_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_con...
function any_of (line 90) | def any_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_con...
function one_of (line 104) | def one_of(predicates: Iterable[Predicate[T_contra]]) -> Predicate[T_con...
FILE: src/phantom/predicates/collection.py
function contains (line 10) | def contains(value: object) -> Predicate[Container]:
function contained (line 20) | def contained(container: Container) -> Predicate[object]:
function count (line 33) | def count(predicate: Predicate[int]) -> Predicate[Sized]:
function exists (line 49) | def exists(predicate: Predicate[_O]) -> Predicate[Iterable]:
function every (line 62) | def every(predicate: Predicate[_O]) -> Predicate[Iterable]:
FILE: src/phantom/predicates/datetime.py
function is_tz_aware (line 6) | def is_tz_aware(dt: datetime.datetime) -> bool:
function is_tz_naive (line 12) | def is_tz_naive(dt: datetime.datetime) -> bool:
FILE: src/phantom/predicates/generic.py
function equal (line 9) | def equal(a: object) -> Predicate[object]:
function identical (line 19) | def identical(a: object) -> Predicate[object]:
function of_type (line 29) | def of_type(t: type | tuple[type, ...]) -> Predicate[object]:
function of_complex_type (line 41) | def of_complex_type(t: type) -> Predicate[object]:
FILE: src/phantom/predicates/interval.py
function exclusive (line 20) | def exclusive(low: T, high: T) -> Predicate[SupportsLtGt[T]]:
function exclusive_inclusive (line 32) | def exclusive_inclusive(low: T, high: T) -> Predicate[SupportsLeGt[T]]:
function inclusive_exclusive (line 44) | def inclusive_exclusive(low: T, high: T) -> Predicate[SupportsLtGe[T]]:
function inclusive (line 56) | def inclusive(low: T, high: T) -> Predicate[SupportsLeGe[T]]:
FILE: src/phantom/predicates/numeric.py
function less (line 18) | def less(n: T) -> Predicate[SupportsLt[T]]:
function le (line 30) | def le(n: T) -> Predicate[SupportsLe[T]]:
function greater (line 43) | def greater(n: T) -> Predicate[SupportsGt[T]]:
function ge (line 56) | def ge(n: T) -> Predicate[SupportsGe[T]]:
function positive (line 69) | def positive(n: SupportsGt[int]) -> bool:
function non_positive (line 74) | def non_positive(n: SupportsLe[int]) -> bool:
function negative (line 79) | def negative(n: SupportsLt[int]) -> bool:
function non_negative (line 84) | def non_negative(n: SupportsGe[int]) -> bool:
function modulo (line 89) | def modulo(n: T, p: Predicate[U]) -> Predicate[SupportsMod[T, U]]:
function even (line 102) | def even(n: int) -> bool:
function odd (line 107) | def odd(n: int) -> bool:
FILE: src/phantom/predicates/re.py
function is_match (line 7) | def is_match(pattern: Pattern[str]) -> Predicate[str]:
function is_full_match (line 20) | def is_full_match(pattern: Pattern[str]) -> Predicate[str]:
FILE: src/phantom/re.py
function _compile (line 28) | def _compile(pattern: Pattern[str] | str) -> Pattern[str]:
class Match (line 34) | class Match(str, Phantom, abstract=True):
method __init_subclass__ (line 43) | def __init_subclass__(cls, pattern: Pattern[str] | str, **kwargs: Any)...
method __schema__ (line 48) | def __schema__(cls) -> Schema:
class FullMatch (line 58) | class FullMatch(str, Phantom, abstract=True):
method __init_subclass__ (line 67) | def __init_subclass__(cls, pattern: Pattern[str] | str, **kwargs: Any)...
method __schema__ (line 72) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 80) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy | None:
FILE: src/phantom/schema.py
class Schema (line 8) | class Schema(TypedDict, total=False):
class SchemaField (line 24) | class SchemaField:
method __modify_schema__ (line 27) | def __modify_schema__(cls, field_schema: dict) -> None:
method __schema__ (line 39) | def __schema__(cls) -> Schema:
FILE: src/phantom/sized.py
class SizedIterable (line 66) | class SizedIterable(Sized, Iterable[T], Protocol[T]):
class SizedIterablePhantomMeta (line 70) | class SizedIterablePhantomMeta(PhantomMeta, _ProtocolMeta): ...
class PhantomSized (line 73) | class PhantomSized(
method __init_subclass__ (line 88) | def __init_subclass__(
method __schema__ (line 102) | def __schema__(cls) -> Schema:
class UnresolvedBounds (line 109) | class UnresolvedBounds(Exception): ...
class LSPViolation (line 112) | class LSPViolation(Exception): ...
class PhantomBound (line 115) | class PhantomBound(
method __init_subclass__ (line 128) | def __init_subclass__(
method __schema__ (line 187) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 205) | def __register_strategy__(cls) -> _hypothesis.HypothesisStrategy:
class NonEmpty (line 237) | class NonEmpty(PhantomBound[T], Generic[T], min=1):
method __schema__ (line 241) | def __schema__(cls) -> Schema:
class NonEmptyStr (line 248) | class NonEmptyStr(str, NonEmpty[str]):
method __schema__ (line 252) | def __schema__(cls) -> Schema:
class Empty (line 259) | class Empty(PhantomBound[T], Generic[T], max=0):
method __schema__ (line 263) | def __schema__(cls) -> Schema:
method __register_strategy__ (line 270) | def __register_strategy__(cls) -> _hypothesis.SearchStrategy:
FILE: tests/ext/test_hypothesis.py
class TensFloat (line 43) | class TensFloat(float, InclusiveExclusive, low=10, high=20): ...
class TensInt (line 46) | class TensInt(int, InclusiveExclusive, low=10, high=20): ...
class Url (line 49) | class Url(
class Few (line 58) | class Few(PhantomBound[T], Generic[T], min=5, max=15): ...
class Inf (line 62) | class Inf:
method __eq__ (line 63) | def __eq__(self, other):
method __lt__ (line 66) | def __lt__(self, other):
class InmappableInc (line 71) | class InmappableInc(int, Inclusive, low=Inf(), high=100): ...
class InmappableExc (line 74) | class InmappableExc(float, Exclusive, low=Inf(), high=100): ...
class InmappableIncExc (line 77) | class InmappableIncExc(int, InclusiveExclusive, low=Inf(), high=100): ...
class InmappableExcInc (line 80) | class InmappableExcInc(float, ExclusiveInclusive, low=Inf(), high=100): ...
class Model (line 84) | class Model:
function test_can_generate_hypothesis_values (line 121) | def test_can_generate_hypothesis_values(model: Model) -> None:
FILE: tests/ext/test_phonenumbers.py
class TestPhoneNumber (line 16) | class TestPhoneNumber:
method test_unparsable_number_is_not_instance (line 17) | def test_unparsable_number_is_not_instance(self):
method test_invalid_number_is_not_instance (line 20) | def test_invalid_number_is_not_instance(self):
method test_unformatted_number_is_instance (line 23) | def test_unformatted_number_is_instance(self):
class TestFormattedPhoneNumber (line 27) | class TestFormattedPhoneNumber:
method test_unparsable_number_is_not_instance (line 28) | def test_unparsable_number_is_not_instance(self):
method test_invalid_number_is_not_instance (line 31) | def test_invalid_number_is_not_instance(self):
method test_unformatted_number_is_not_instance (line 34) | def test_unformatted_number_is_not_instance(self):
method test_formatted_number_is_instance (line 37) | def test_formatted_number_is_instance(self):
method test_normalizes_unformatted_number (line 40) | def test_normalizes_unformatted_number(self):
method test_parse_raises_for_invalid_phone_number (line 45) | def test_parse_raises_for_invalid_phone_number(self):
method test_raises_type_error_for_out_of_bound_type (line 49) | def test_raises_type_error_for_out_of_bound_type(self):
class TestDeconstructPhoneNumber (line 58) | class TestDeconstructPhoneNumber:
method test_can_parse_international_phone_number_without_country_code (line 59) | def test_can_parse_international_phone_number_without_country_code(self):
method test_can_parse_international_phone_number_with_country_code (line 64) | def test_can_parse_international_phone_number_with_country_code(self):
method test_can_parse_national_phone_number_with_country_code (line 69) | def test_can_parse_national_phone_number_with_country_code(self):
method test_raises_invalid_phone_number_for_insufficient_country_data (line 74) | def test_raises_invalid_phone_number_for_insufficient_country_data(self):
method test_raises_invalid_phone_number_for_parse_exception (line 79) | def test_raises_invalid_phone_number_for_parse_exception(self):
method test_raises_invalid_phone_number_for_invalid_phone_number (line 84) | def test_raises_invalid_phone_number_for_invalid_phone_number(self):
class TestNormalizePhoneNumber (line 90) | class TestNormalizePhoneNumber:
method test_can_normalize_national_number_with_country_code (line 91) | def test_can_normalize_national_number_with_country_code(self):
method test_can_normalize_international_number_without_country_code (line 94) | def test_can_normalize_international_number_without_country_code(self):
class TestIsPhoneNumber (line 98) | class TestIsPhoneNumber:
method test_returns_true_for_valid_number (line 99) | def test_returns_true_for_valid_number(self):
method test_returns_false_for_invalid_number (line 102) | def test_returns_false_for_invalid_number(self):
class TestIsFormattedPhoneNumber (line 106) | class TestIsFormattedPhoneNumber:
method test_returns_true_for_formatted_number (line 107) | def test_returns_true_for_formatted_number(self) -> None:
method test_returns_false_for_unformatted_number (line 112) | def test_returns_false_for_unformatted_number(self):
FILE: tests/predicates/test_boolean.py
class TestTrue (line 11) | class TestTrue:
method test_returns_true_for_any_given_value (line 13) | def test_returns_true_for_any_given_value(self, value: object) -> None:
class TestFalse (line 17) | class TestFalse:
method test_returns_false_for_any_given_value (line 19) | def test_returns_false_for_any_given_value(self, value: object) -> None:
class TestNegate (line 23) | class TestNegate:
method test_can_negate_true (line 24) | def test_can_negate_true(self):
method test_can_negate_false (line 27) | def test_can_negate_false(self):
method test_repr_contains_bound_parameter (line 30) | def test_repr_contains_bound_parameter(self):
class TestTruthy (line 38) | class TestTruthy:
method test_returns_true_for_truthy_value (line 40) | def test_returns_true_for_truthy_value(self, value: object) -> None:
method test_returns_false_for_falsy_value (line 44) | def test_returns_false_for_falsy_value(self, value: object) -> None:
class TestFalsy (line 48) | class TestFalsy:
method test_returns_true_for_falsy_value (line 50) | def test_returns_true_for_falsy_value(self, value: object) -> None:
method test_returns_false_for_truthy_value (line 54) | def test_returns_false_for_truthy_value(self, value: object) -> None:
class TestBoth (line 58) | class TestBoth:
method test_returns_true_for_two_succeeding_predicates (line 59) | def test_returns_true_for_two_succeeding_predicates(self) -> None:
method test_returns_false_for_falsy_predicate (line 70) | def test_returns_false_for_falsy_predicate(
method test_repr_contains_bound_parameter (line 75) | def test_repr_contains_bound_parameter(self):
class TestEither (line 81) | class TestEither:
method test_returns_true_for_truthy_predicate (line 90) | def test_returns_true_for_truthy_predicate(
method test_returns_false_for_two_falsy_predicates (line 95) | def test_returns_false_for_two_falsy_predicates(self) -> None:
method test_repr_contains_bound_parameter (line 98) | def test_repr_contains_bound_parameter(self):
class TestXor (line 104) | class TestXor:
method test_returns_true_for_two_different_bools (line 112) | def test_returns_true_for_two_different_bools(
method test_returns_false_for_two_equal_bools (line 124) | def test_returns_false_for_two_equal_bools(
method test_repr_contains_bound_parameter (line 129) | def test_repr_contains_bound_parameter(self):
class TestAllOf (line 161) | class TestAllOf:
method test_returns_true_for_empty_set_of_predicates (line 162) | def test_returns_true_for_empty_set_of_predicates(self) -> None:
method test_returns_true_for_succeeding_predicates (line 167) | def test_returns_true_for_succeeding_predicates(
method test_returns_false_for_some_failing_predicate (line 173) | def test_returns_false_for_some_failing_predicate(
method test_returns_false_for_only_failing_predicate (line 179) | def test_returns_false_for_only_failing_predicate(
method test_materializes_generated_predicates (line 185) | def test_materializes_generated_predicates(
method test_repr_contains_bound_parameter (line 192) | def test_repr_contains_bound_parameter(self):
class TestAnyOf (line 198) | class TestAnyOf:
method test_returns_false_for_empty_set_of_predicates (line 199) | def test_returns_false_for_empty_set_of_predicates(self) -> None:
method test_returns_true_for_succeeding_predicates (line 204) | def test_returns_true_for_succeeding_predicates(
method test_returns_true_for_some_failing_predicate (line 210) | def test_returns_true_for_some_failing_predicate(
method test_returns_false_for_only_failing_predicate (line 216) | def test_returns_false_for_only_failing_predicate(
method test_materializes_generated_predicates (line 222) | def test_materializes_generated_predicates(
method test_repr_contains_bound_parameter (line 229) | def test_repr_contains_bound_parameter(self):
class TestOneOf (line 235) | class TestOneOf:
method test_returns_false_for_empty_set_of_predicates (line 236) | def test_returns_false_for_empty_set_of_predicates(self) -> None:
method test_returns_false_for_more_than_one_succeeding_predicates (line 250) | def test_returns_false_for_more_than_one_succeeding_predicates(
method test_returns_true_for_one_succeeding_predicate (line 266) | def test_returns_true_for_one_succeeding_predicate(
method test_returns_false_for_only_failing_predicate (line 272) | def test_returns_false_for_only_failing_predicate(
method test_materializes_generated_predicates (line 285) | def test_materializes_generated_predicates(
method test_repr_contains_bound_parameter (line 292) | def test_repr_contains_bound_parameter(self):
FILE: tests/predicates/test_collection.py
class TestContains (line 15) | class TestContains:
method test_returns_true_for_container_with_item (line 23) | def test_returns_true_for_container_with_item(
method test_returns_false_for_container_without_item (line 35) | def test_returns_false_for_container_without_item(
method test_repr_contains_bound_parameter (line 40) | def test_repr_contains_bound_parameter(self):
class TestContained (line 46) | class TestContained:
method test_returns_true_for_item_in_container (line 54) | def test_returns_true_for_item_in_container(
method test_returns_false_for_item_not_in_container (line 66) | def test_returns_false_for_item_not_in_container(
method test_repr_contains_bound_parameter (line 71) | def test_repr_contains_bound_parameter(self):
class TestCount (line 77) | class TestCount:
method test_returns_true_for_size_matching_predicate (line 86) | def test_returns_true_for_size_matching_predicate(
method test_returns_false_for_size_failing_predicate (line 99) | def test_returns_false_for_size_failing_predicate(
method test_repr_contains_bound_parameter (line 104) | def test_repr_contains_bound_parameter(self):
class TestExists (line 108) | class TestExists:
method test_returns_true_for_iterable_containing_satisfying_item (line 117) | def test_returns_true_for_iterable_containing_satisfying_item(
method test_returns_false_for_iterable_not_containing_satisfying_item (line 130) | def test_returns_false_for_iterable_not_containing_satisfying_item(
method test_repr_contains_bound_parameter (line 135) | def test_repr_contains_bound_parameter(self):
class TestEvery (line 141) | class TestEvery:
method test_returns_true_for_complete_iterable (line 151) | def test_returns_true_for_complete_iterable(
method test_returns_false_for_incomplete_iterable (line 164) | def test_returns_false_for_incomplete_iterable(
method test_repr_contains_bound_parameter (line 169) | def test_repr_contains_bound_parameter(self):
FILE: tests/predicates/test_datetime.py
class TestIsTZAware (line 16) | class TestIsTZAware:
method test_returns_true_for_aware_dt (line 18) | def test_returns_true_for_aware_dt(self, dt: datetime.datetime) -> None:
method test_returns_false_for_naive_dt (line 22) | def test_returns_false_for_naive_dt(self, dt: datetime.datetime) -> None:
class TestIsTZNaive (line 26) | class TestIsTZNaive:
method test_returns_true_for_naive_dt (line 28) | def test_returns_true_for_naive_dt(self, dt: datetime.datetime) -> None:
method test_returns_false_for_aware_dt (line 32) | def test_returns_false_for_aware_dt(self, dt: datetime.datetime) -> None:
FILE: tests/predicates/test_generic.py
class TestEqual (line 8) | class TestEqual:
method test_returns_true_for_equal_values (line 10) | def test_returns_true_for_equal_values(self, a: object, b: object) -> ...
method test_returns_false_for_non_equal_values (line 15) | def test_returns_false_for_non_equal_values(self, a: object, b: object...
method test_repr_contains_bound_parameter (line 19) | def test_repr_contains_bound_parameter(self):
class TestIdentical (line 23) | class TestIdentical:
method test_returns_true_for_identical_values (line 27) | def test_returns_true_for_identical_values(self, a: object, b: object)...
method test_returns_false_for_different_values (line 32) | def test_returns_false_for_different_values(self, a: object, b: object...
method test_repr_contains_bound_parameter (line 36) | def test_repr_contains_bound_parameter(self):
class TestOfType (line 40) | class TestOfType:
method test_returns_true_for_instance_of_types (line 42) | def test_returns_true_for_instance_of_types(
method test_returns_false_for_instance_of_other_type (line 50) | def test_returns_false_for_instance_of_other_type(
method test_repr_contains_bound_parameter (line 57) | def test_repr_contains_bound_parameter(self):
FILE: tests/predicates/test_interval.py
class TestExclusive (line 40) | class TestExclusive:
method test_returns_true_for_middle_value (line 41) | def test_returns_true_for_middle_value(self) -> None:
method test_returns_true_for_inside_value (line 45) | def test_returns_true_for_inside_value(
method test_returns_false_for_edge_value (line 51) | def test_returns_false_for_edge_value(
method test_returns_false_for_outside_value (line 57) | def test_returns_false_for_outside_value(
method test_repr_contains_bound_parameter (line 62) | def test_repr_contains_bound_parameter(self):
class TestInclusive (line 66) | class TestInclusive:
method test_returns_true_for_middle_value (line 67) | def test_returns_true_for_middle_value(self) -> None:
method test_returns_true_for_inside_value (line 71) | def test_returns_true_for_inside_value(
method test_returns_true_for_edge_value (line 77) | def test_returns_true_for_edge_value(
method test_returns_false_for_outside_value (line 83) | def test_returns_false_for_outside_value(
method test_repr_contains_bound_parameter (line 88) | def test_repr_contains_bound_parameter(self):
class TestInclusiveExclusive (line 92) | class TestInclusiveExclusive:
method test_returns_true_for_middle_value (line 93) | def test_returns_true_for_middle_value(self) -> None:
method test_lower_bound (line 96) | def test_lower_bound(self) -> None:
method test_upper_bound (line 101) | def test_upper_bound(self) -> None:
method test_repr_contains_bound_parameter (line 106) | def test_repr_contains_bound_parameter(self):
class TestExclusiveInclusive (line 112) | class TestExclusiveInclusive:
method test_returns_true_for_middle_value (line 113) | def test_returns_true_for_middle_value(self) -> None:
method test_lower_bound (line 116) | def test_lower_bound(self) -> None:
method test_upper_bound (line 121) | def test_upper_bound(self) -> None:
method test_repr_contains_bound_parameter (line 126) | def test_repr_contains_bound_parameter(self):
FILE: tests/predicates/test_numeric.py
class TestLess (line 8) | class TestLess:
method test_returns_true_for_values_below_limit (line 9) | def test_returns_true_for_values_below_limit(self) -> None:
method test_returns_false_for_values_above_limit (line 15) | def test_returns_false_for_values_above_limit(self) -> None:
method test_repr_contains_bound_parameter (line 21) | def test_repr_contains_bound_parameter(self):
class TestLE (line 25) | class TestLE:
method test_returns_true_for_values_below_limit (line 26) | def test_returns_true_for_values_below_limit(self) -> None:
method test_returns_false_for_values_above_limit (line 33) | def test_returns_false_for_values_above_limit(self) -> None:
method test_repr_contains_bound_parameter (line 39) | def test_repr_contains_bound_parameter(self):
class TestGreater (line 43) | class TestGreater:
method test_returns_true_for_values_above_limit (line 44) | def test_returns_true_for_values_above_limit(self) -> None:
method test_returns_false_for_values_below_limit (line 50) | def test_returns_false_for_values_below_limit(self) -> None:
method test_repr_contains_bound_parameter (line 56) | def test_repr_contains_bound_parameter(self):
class TestGE (line 60) | class TestGE:
method test_returns_true_for_values_above_limit (line 61) | def test_returns_true_for_values_above_limit(self) -> None:
method test_returns_false_for_values_below_limit (line 68) | def test_returns_false_for_values_below_limit(self) -> None:
method test_repr_contains_bound_parameter (line 74) | def test_repr_contains_bound_parameter(self):
class TestPositive (line 78) | class TestPositive:
method test_limits (line 79) | def test_limits(self) -> None:
class TestNonPositive (line 86) | class TestNonPositive:
method test_limits (line 87) | def test_limits(self) -> None:
class TestNegative (line 94) | class TestNegative:
method test_limits (line 95) | def test_limits(self) -> None:
class TestNonNegative (line 102) | class TestNonNegative:
method test_limits (line 103) | def test_limits(self) -> None:
class TestModulo (line 110) | class TestModulo:
method test_repr_contains_bound_parameter (line 111) | def test_repr_contains_bound_parameter(self):
class TestEven (line 121) | class TestEven:
method test_returns_true_for_even_value (line 123) | def test_returns_true_for_even_value(self, value: int) -> None:
method test_returns_false_for_odd_value (line 127) | def test_returns_false_for_odd_value(self, value: int) -> None:
class TestOdd (line 131) | class TestOdd:
method test_returns_true_for_odd_value (line 133) | def test_returns_true_for_odd_value(self, value: int) -> None:
method test_returns_false_for_even_value (line 137) | def test_returns_false_for_even_value(self, value: int) -> None:
FILE: tests/predicates/test_re.py
class TestIsMatch (line 13) | class TestIsMatch:
method test_returns_true_for_matching_string (line 14) | def test_returns_true_for_matching_string(self) -> None:
method test_returns_false_for_non_matching_string (line 18) | def test_returns_false_for_non_matching_string(self) -> None:
method test_repr_contains_bound_parameter (line 22) | def test_repr_contains_bound_parameter(self):
class TestIsFullMatch (line 26) | class TestIsFullMatch:
method test_returns_true_for_matching_string (line 27) | def test_returns_true_for_matching_string(self) -> None:
method test_returns_false_for_non_matching_string (line 30) | def test_returns_false_for_non_matching_string(self) -> None:
method test_repr_contains_bound_parameter (line 35) | def test_repr_contains_bound_parameter(self):
FILE: tests/predicates/test_utils.py
function foo (line 8) | def foo(a: int, b: int, c: int) -> bool:
class TestFunctionRepr (line 12) | class TestFunctionRepr:
method test_explodes_partial_arguments (line 13) | def test_explodes_partial_arguments(self):
FILE: tests/predicates/utils.py
function assert_predicate_name_equals (line 4) | def assert_predicate_name_equals(predicate: Predicate, expected_name: st...
FILE: tests/pydantic/test_datetime.py
class HasTZAware (line 15) | class HasTZAware(pydantic.BaseModel):
class TestPydanticTZAware (line 19) | class TestPydanticTZAware:
method test_can_parse_tz_aware (line 21) | def test_can_parse_tz_aware(self, value: str, expected: datetime.datet...
method test_tz_aware_rejects_naive_datetime (line 26) | def test_tz_aware_rejects_naive_datetime(self):
class HasTZNaive (line 31) | class HasTZNaive(pydantic.BaseModel):
class TestPydanticTZNaive (line 35) | class TestPydanticTZNaive:
method test_can_parse_tz_naive (line 37) | def test_can_parse_tz_naive(self, value: str, expected: datetime.datet...
method test_tz_naive_rejects_aware_datetime (line 42) | def test_tz_naive_rejects_aware_datetime(self):
FILE: tests/pydantic/test_schemas.py
class ExclusiveType (line 28) | class ExclusiveType(int, Exclusive, low=0, high=100): ...
class InclusiveType (line 31) | class InclusiveType(float, Inclusive, low=-1, high=1): ...
class ExclusiveInclusiveType (line 34) | class ExclusiveInclusiveType(float, ExclusiveInclusive, low=0, high=100)...
class InclusiveExclusiveType (line 37) | class InclusiveExclusiveType(float, InclusiveExclusive, low=-100, high=0...
class MatchType (line 40) | class MatchType(Match, pattern=r"^[A-Z]{2}[0-9]{2}$"): ...
class FullMatchType (line 43) | class FullMatchType(FullMatch, pattern=r"^[A-Z]{2}[0-9]{2}$"): ...
class OddSize (line 46) | class OddSize(PhantomSized[int], len=odd): ...
class DataModel (line 49) | class DataModel(pydantic.BaseModel):
class TestShippedTypesImplementsSchema (line 71) | class TestShippedTypesImplementsSchema:
method test_interval_open_implements_schema (line 72) | def test_interval_open_implements_schema(self):
method test_interval_closed_implements_schema (line 81) | def test_interval_closed_implements_schema(self):
method test_interval_exclusive_inclusive_implements_schema (line 90) | def test_interval_exclusive_inclusive_implements_schema(self):
method test_interval_inclusive_exclusive_implements_schema (line 99) | def test_interval_inclusive_exclusive_implements_schema(self):
method test_interval_negative_int_implements_schema (line 108) | def test_interval_negative_int_implements_schema(self):
method test_interval_natural_implements_schema (line 116) | def test_interval_natural_implements_schema(self):
method test_interval_portion_implements_schema (line 124) | def test_interval_portion_implements_schema(self):
method test_tz_aware_implements_schema (line 133) | def test_tz_aware_implements_schema(self):
method test_tz_naive_implements_schema (line 141) | def test_tz_naive_implements_schema(self):
method test_re_match_implements_schema (line 149) | def test_re_match_implements_schema(self):
method test_re_full_match_implements_schema (line 159) | def test_re_full_match_implements_schema(self):
method test_sized_non_empty_implements_schema (line 167) | def test_sized_non_empty_implements_schema(self):
method test_sized_empty_implements_schema (line 176) | def test_sized_empty_implements_schema(self):
method test_sized_non_empty_str_implements_schema (line 184) | def test_sized_non_empty_str_implements_schema(self):
method test_phantom_sized_implements_schema (line 192) | def test_phantom_sized_implements_schema(self):
method test_country_code_implements_schema (line 198) | def test_country_code_implements_schema(self):
method test_phone_number_implements_schema (line 207) | def test_phone_number_implements_schema(self):
method test_formatted_phone_number_implements_schema (line 215) | def test_formatted_phone_number_implements_schema(self):
method test_sequence_not_str_implements_schema (line 223) | def test_sequence_not_str_implements_schema(self):
FILE: tests/test_base.py
class TestParseBound (line 20) | class TestParseBound:
method test_can_parse_simple_bound (line 21) | def test_can_parse_simple_bound(self):
method test_raises_for_invalid_value (line 25) | def test_raises_for_invalid_value(self):
method test_raises_for_invalid_intersection (line 33) | def test_raises_for_invalid_intersection(self):
method test_raises_for_invalid_union (line 41) | def test_raises_for_invalid_union(self):
method test_raises_for_invalid_pep_604_union (line 50) | def test_raises_for_invalid_pep_604_union(self):
method test_can_parse_intersection (line 58) | def test_can_parse_intersection(self):
method test_can_parse_union (line 69) | def test_can_parse_union(self):
class TestPhantom (line 85) | class TestPhantom:
method test_subclass_without_predicate_raises (line 86) | def test_subclass_without_predicate_raises(self):
method test_subclass_without_bound_raises (line 93) | def test_subclass_without_bound_raises(self):
method test_rejects_partial_bound (line 100) | def test_rejects_partial_bound(self):
method test_concrecte_subclass_of_abstract_raises_for_missing_class_attribute (line 105) | def test_concrecte_subclass_of_abstract_raises_for_missing_class_attri...
method test_can_subclass_without_predicate_if_abstract (line 114) | def test_can_subclass_without_predicate_if_abstract(self):
method test_can_subclass_without_bound_if_abstract (line 117) | def test_can_subclass_without_bound_if_abstract(self):
method test_subclass_with_incompatible_bounds_raises (line 120) | def test_subclass_with_incompatible_bounds_raises(self):
method test_can_define_bound_implicitly (line 127) | def test_can_define_bound_implicitly(self):
method test_can_define_bound_explicitly (line 132) | def test_can_define_bound_explicitly(self):
method test_can_inherit_bound (line 137) | def test_can_inherit_bound(self):
method test_raises_mutable_type_for_mutable_bound_type (line 153) | def test_raises_mutable_type_for_mutable_bound_type(self, bound_type: ...
method test_can_use_frozen_dataclass_as_bound (line 162) | def test_can_use_frozen_dataclass_as_bound(self):
method test_abstract_instance_check_raises (line 168) | def test_abstract_instance_check_raises(self):
method test_phantom_meta_is_usable_without_phantom_base (line 174) | def test_phantom_meta_is_usable_without_phantom_base(self):
FILE: tests/test_boolean.py
class TestTruthy (line 10) | class TestTruthy:
method test_truthy_value_is_instance (line 12) | def test_truthy_value_is_instance(self, v):
method test_falsy_value_is_not_instance (line 16) | def test_falsy_value_is_not_instance(self, v):
method test_instantiation_returns_instance (line 20) | def test_instantiation_returns_instance(self, v):
method test_instantiation_raises_for_falsy_value (line 24) | def test_instantiation_raises_for_falsy_value(self, v):
class TestFalsy (line 29) | class TestFalsy:
method test_falsy_value_is_instance (line 31) | def test_falsy_value_is_instance(self, v):
method test_truthy_value_is_not_instance (line 35) | def test_truthy_value_is_not_instance(self, v):
method test_instantiation_returns_instance (line 39) | def test_instantiation_returns_instance(self, v):
method test_instantiation_raises_for_truthy_value (line 43) | def test_instantiation_raises_for_truthy_value(self, v):
FILE: tests/test_datetime.py
class TestTZAware (line 92) | class TestTZAware:
method test_aware_datetime_is_instance (line 94) | def test_aware_datetime_is_instance(self, dt: datetime.datetime):
method test_naive_datetime_is_not_instance (line 98) | def test_naive_datetime_is_not_instance(self, dt: datetime.datetime):
method test_instantiation_raises_for_naive_datetime_instance (line 102) | def test_instantiation_raises_for_naive_datetime_instance(
method test_instantiation_returns_instance (line 109) | def test_instantiation_returns_instance(self, dt: datetime.datetime):
method test_parse_rejects_non_str_object (line 113) | def test_parse_rejects_non_str_object(self, value: object):
method test_parse_rejects_invalid_str (line 119) | def test_parse_rejects_invalid_str(self, value: object):
method test_parse_rejects_naive_str (line 125) | def test_parse_rejects_naive_str(self, value: str, expected: datetime....
method test_can_parse_valid_str (line 131) | def test_can_parse_valid_str(self, value: str, expected: datetime.date...
method test_parse_str_without_dateutil_raises_missing_dependency (line 136) | def test_parse_str_without_dateutil_raises_missing_dependency(
class TestTZNaive (line 145) | class TestTZNaive:
method test_naive_datetime_is_instance (line 147) | def test_naive_datetime_is_instance(self, dt: datetime.datetime):
method test_aware_datetime_is_not_instance (line 151) | def test_aware_datetime_is_not_instance(self, dt: datetime.datetime):
method test_instantiation_raises_for_aware_datetime_instance (line 155) | def test_instantiation_raises_for_aware_datetime_instance(
method test_instantiation_returns_instance (line 162) | def test_instantiation_returns_instance(self, dt: datetime.datetime):
method test_parse_rejects_non_str_object (line 166) | def test_parse_rejects_non_str_object(self, value: object):
method test_parse_rejects_invalid_str (line 172) | def test_parse_rejects_invalid_str(self, value: object):
method test_parse_rejects_aware_str (line 178) | def test_parse_rejects_aware_str(self, value: str, expected: datetime....
method test_can_parse_valid_str (line 184) | def test_can_parse_valid_str(self, value: str, expected: datetime.date...
method test_parse_str_without_dateutil_raises_missing_dependency (line 189) | def test_parse_str_without_dateutil_raises_missing_dependency(
FILE: tests/test_fn.py
class Test_name (line 24) | class Test_name:
class Nested (line 25) | class Nested:
method method (line 26) | def method(self): ...
method test_can_get_name_of (line 38) | def test_can_get_name_of(self, function: Callable, expected: str) -> N...
function reversed_str (line 42) | def reversed_str(value: str) -> str:
class TestCompose2 (line 51) | class TestCompose2:
method test_can_compose_two (line 61) | def test_can_compose_two(
method test_can_compose_complex_predicate (line 73) | def test_can_compose_complex_predicate(self) -> None:
class BaseError (line 86) | class BaseError(Exception): ...
class Error (line 89) | class Error(BaseError): ...
class ErrorA (line 92) | class ErrorA(Error): ...
class ErrorB (line 95) | class ErrorB(Error): ...
function dummy_function (line 98) | def dummy_function(val: type[Exception]) -> None:
class TestExcepts (line 103) | class TestExcepts:
method test_returns_bool (line 114) | def test_returns_bool(
method test_reraises (line 122) | def test_reraises(self) -> None:
FILE: tests/test_interval.py
class TestInterval (line 24) | class TestInterval:
method test_subclassing_without_check_raises (line 25) | def test_subclassing_without_check_raises(self):
method test_parse_coerces_str (line 30) | def test_parse_coerces_str(self):
method test_allows_decimal_bound (line 35) | def test_allows_decimal_bound(self):
method test_subclass_inherits_bounds (line 48) | def test_subclass_inherits_bounds(self):
class TestNegativeInt (line 80) | class TestNegativeInt:
method test_negative_int_is_instance (line 82) | def test_negative_int_is_instance(self, i):
method test_positive_int_is_not_instance (line 85) | def test_positive_int_is_not_instance(self):
method test_instantiation_raises_for_positive_int (line 89) | def test_instantiation_raises_for_positive_int(self):
method test_instantiation_returns_instance (line 96) | def test_instantiation_returns_instance(self, i):
class TestNatural (line 100) | class TestNatural:
method test_positive_int_is_instance (line 102) | def test_positive_int_is_instance(self, i):
method test_negative_int_is_not_instance (line 105) | def test_negative_int_is_not_instance(self):
method test_instantiation_raises_for_positive_int (line 109) | def test_instantiation_raises_for_positive_int(self):
method test_instantiation_returns_instance (line 118) | def test_instantiation_returns_instance(self, i):
class TestPortion (line 130) | class TestPortion:
method test_value_inside_range_is_instance (line 132) | def test_value_inside_range_is_instance(self, i):
method test_value_outside_range_is_instance (line 136) | def test_value_outside_range_is_instance(self, i):
method test_instantiation_returns_instance (line 140) | def test_instantiation_returns_instance(self, i):
method test_instantiation_raises_for_non_portion_values (line 145) | def test_instantiation_raises_for_non_portion_values(self, i):
class TestGetScalarIntBounds (line 152) | class TestGetScalarIntBounds:
method test_returns_correct_bounds (line 164) | def test_returns_correct_bounds(
method test_raises_non_scalar_bounds_for_non_int_lower_bound (line 176) | def test_raises_non_scalar_bounds_for_non_int_lower_bound(self):
method test_raises_non_scalar_bounds_for_non_int_upper_bound (line 190) | def test_raises_non_scalar_bounds_for_non_int_upper_bound(self):
class TestGetScalarFloatBounds (line 205) | class TestGetScalarFloatBounds:
method test_returns_correct_bounds (line 215) | def test_returns_correct_bounds(
method test_raises_non_scalar_bounds_for_non_int_lower_bound (line 225) | def test_raises_non_scalar_bounds_for_non_int_lower_bound(self):
method test_raises_non_scalar_bounds_for_non_int_upper_bound (line 239) | def test_raises_non_scalar_bounds_for_non_int_upper_bound(self):
FILE: tests/test_iso3166.py
class TestNormalizeAlpha2CountryCode (line 8) | class TestNormalizeAlpha2CountryCode:
method test_normalizes_mixed_case_valid_country_code (line 13) | def test_normalizes_mixed_case_valid_country_code(
method test_raises_for_invalid_country_code (line 19) | def test_raises_for_invalid_country_code(self, invalid: str) -> None:
class TestAlpha2 (line 24) | class TestAlpha2:
method test_invalid_country_code_is_not_instance (line 26) | def test_invalid_country_code_is_not_instance(self, invalid: object) -...
method test_valid_country_code_is_instance (line 30) | def test_valid_country_code_is_instance(self, country_code: str) -> None:
method test_normalizes_valid_country_code (line 33) | def test_normalizes_valid_country_code(self) -> None:
method test_raises_for_invalid_country_code (line 39) | def test_raises_for_invalid_country_code(self, invalid: str) -> None:
FILE: tests/test_negated.py
class TestSequenceNotStr (line 34) | class TestSequenceNotStr:
method test_is_instance (line 36) | def test_is_instance(self, value: object):
method test_is_not_instance (line 40) | def test_is_not_instance(self, value: object):
method test_parse_returns_instance (line 44) | def test_parse_returns_instance(self, value: object):
method test_parse_raises_for_non_instances (line 48) | def test_parse_raises_for_non_instances(self, value: object):
method test_subscription_returns_type_alias (line 52) | def test_subscription_returns_type_alias(self):
FILE: tests/test_re.py
class MatchPatternInstance (line 11) | class MatchPatternInstance(Match, pattern=re.compile(r"abc")): ...
class MatchPatternStr (line 14) | class MatchPatternStr(Match, pattern=r"abc"): ...
class TestMatch (line 22) | class TestMatch:
method test_non_matching_string_is_not_instance (line 24) | def test_non_matching_string_is_not_instance(self, match_type: type[Ma...
method test_matching_string_is_instance (line 28) | def test_matching_string_is_instance(self, match_type: type[Match]):
method test_instantiation_raises_for_non_matching_string (line 32) | def test_instantiation_raises_for_non_matching_string(
method test_instantiation_returns_instance (line 39) | def test_instantiation_returns_instance(self, match_type: type[Match]):
class FullMatchPatternInstance (line 44) | class FullMatchPatternInstance(FullMatch, pattern=re.compile(r"abc")): ...
class FullMatchStr (line 47) | class FullMatchStr(FullMatch, pattern=r"abc"): ...
class TestFullMatch (line 55) | class TestFullMatch:
method test_non_matching_string_is_not_instance (line 57) | def test_non_matching_string_is_not_instance(
method test_matching_string_is_instance (line 63) | def test_matching_string_is_instance(self, full_match_type: type[FullM...
method test_instantiation_raises_for_non_matching_string (line 67) | def test_instantiation_raises_for_non_matching_string(
method test_instantiation_returns_instance (line 74) | def test_instantiation_returns_instance(self, full_match_type: type[Fu...
FILE: tests/test_sized.py
class MutableDataclass (line 21) | class MutableDataclass:
class OddSize (line 42) | class OddSize(PhantomSized[T], Generic[T], len=odd): ...
class TestPhantomSized (line 45) | class TestPhantomSized:
method test_value_fulfilling_predicate_is_instance (line 68) | def test_value_fulfilling_predicate_is_instance(self, container: object):
method test_value_not_fulfilling_predicate_is_not_instance (line 72) | def test_value_not_fulfilling_predicate_is_not_instance(self, containe...
method test_instantiation_raises_for_invalid_length (line 76) | def test_instantiation_raises_for_invalid_length(self, container: obje...
method test_instantiation_raises_for_mutable (line 81) | def test_instantiation_raises_for_mutable(self, container: object):
method test_instantiation_returns_instance (line 86) | def test_instantiation_returns_instance(self, container: object):
method test_subscription_returns_type_alias (line 89) | def test_subscription_returns_type_alias(self):
class Tens (line 96) | class Tens(PhantomBound[T], Generic[T], min=10, max=19): ...
class TestPhantomBound (line 99) | class TestPhantomBound:
method test_value_fulfilling_predicate_is_instance (line 110) | def test_value_fulfilling_predicate_is_instance(self, container: object):
method test_value_not_fulfilling_predicate_is_not_instance (line 114) | def test_value_not_fulfilling_predicate_is_not_instance(self, containe...
method test_instantiation_raises_for_invalid_length (line 118) | def test_instantiation_raises_for_invalid_length(self, container: obje...
method test_instantiation_raises_for_mutable (line 123) | def test_instantiation_raises_for_mutable(self, container: object):
method test_instantiation_returns_instance (line 128) | def test_instantiation_returns_instance(self, container: object):
method test_subscription_returns_type_alias (line 131) | def test_subscription_returns_type_alias(self):
method test_raises_lsp_violation_when_attempting_to_decrease_min (line 137) | def test_raises_lsp_violation_when_attempting_to_decrease_min(self):
method test_raises_lsp_violation_when_attempting_to_increase_max (line 142) | def test_raises_lsp_violation_when_attempting_to_increase_max(self):
method test_can_narrow_range_in_subclass (line 147) | def test_can_narrow_range_in_subclass(self):
method test_abstract_subclass_can_omit_bounds (line 155) | def test_abstract_subclass_can_omit_bounds(self):
method test_raises_unresolved_bounds_when_concrete_subclass_omits_bounds (line 163) | def test_raises_unresolved_bounds_when_concrete_subclass_omits_bounds(...
class TestNonEmpty (line 169) | class TestNonEmpty:
method test_non_empty_container_is_instance (line 171) | def test_non_empty_container_is_instance(self, container):
method test_empty_container_is_instance (line 175) | def test_empty_container_is_instance(self, container):
method test_instantiation_raises_for_empty_container (line 179) | def test_instantiation_raises_for_empty_container(self, container):
method test_instantiation_raises_for_mutable (line 184) | def test_instantiation_raises_for_mutable(self, container):
method test_instantiation_returns_instance (line 189) | def test_instantiation_returns_instance(self, container):
method test_subscription_returns_type_alias (line 192) | def test_subscription_returns_type_alias(self):
class TestEmpty (line 199) | class TestEmpty:
method test_non_empty_container_is_instance (line 201) | def test_non_empty_container_is_instance(self, container):
method test_empty_container_is_instance (line 205) | def test_empty_container_is_instance(self, container):
method test_instantiation_raises_for_non_empty_container (line 209) | def test_instantiation_raises_for_non_empty_container(self, container):
method test_instantiation_raises_for_mutable (line 214) | def test_instantiation_raises_for_mutable(self, container):
method test_instantiation_returns_instance (line 219) | def test_instantiation_returns_instance(self, container):
method test_subscription_returns_type_alias (line 222) | def test_subscription_returns_type_alias(self):
class TestNonEmptyStr (line 235) | class TestNonEmptyStr:
method test_non_empty_str_is_instance (line 237) | def test_non_empty_str_is_instance(self, value: str):
method test_empty_str_is_not_instance (line 240) | def test_empty_str_is_not_instance(self):
method test_instantiation_raises_for_empty_str (line 243) | def test_instantiation_raises_for_empty_str(self):
method test_instantiation_raises_for_non_str (line 251) | def test_instantiation_raises_for_non_str(self, value: object):
method test_instantiation_returns_instance (line 256) | def test_instantiation_returns_instance(self, value: str):
FILE: tests/test_utils.py
class A (line 9) | class A: ...
class B (line 12) | class B: ...
class C (line 15) | class C: ...
class AAndB (line 18) | class AAndB(A, B): ...
class SubOfA (line 21) | class SubOfA(A): ...
class TestIsSubtype (line 24) | class TestIsSubtype:
method test_returns_true_for_valid_subtype (line 50) | def test_returns_true_for_valid_subtype(self, a: BoundType, b: BoundTy...
method test_returns_false_for_valid_subtype (line 80) | def test_returns_false_for_valid_subtype(self, a: BoundType, b: BoundT...
FILE: tests/types.py
class FloatInc (line 7) | class FloatInc(float, Inclusive, low=0, high=100): ...
class IntInc (line 10) | class IntInc(int, Inclusive, low=0, high=100): ...
class FloatExc (line 13) | class FloatExc(float, Exclusive, low=0, high=100): ...
class IntExc (line 16) | class IntExc(int, Exclusive, low=0, high=100): ...
class FloatIncExc (line 19) | class FloatIncExc(float, InclusiveExclusive, low=0, high=100): ...
class IntIncExc (line 22) | class IntIncExc(int, InclusiveExclusive, low=0, high=100): ...
class FloatExcInc (line 25) | class FloatExcInc(float, ExclusiveInclusive, low=0, high=100): ...
class IntExcInc (line 28) | class IntExcInc(int, ExclusiveInclusive, low=0, high=100): ...
Condensed preview — 104 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (319K chars).
[
{
"path": ".editorconfig",
"chars": 226,
"preview": "root = True\n\n[*]\nend_of_line = lf\ninsert_final_newline = True\nindent_style = space\nindent_size = 4\ntrim_trailing_whitesp"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 5766,
"preview": "# Citizen Code of Conduct\n\n## 1. Purpose\n\nA primary goal of Phantom Types is to be inclusive to the largest number of\nco"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 1044,
"preview": "## Contributing Guidelines\n\nAll sorts of contributions are welcome, ranging from raising ideas, reporting bugs,\nhelping "
},
{
"path": ".github/dependabot.yaml",
"chars": 328,
"preview": "# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.y"
},
{
"path": ".github/workflows/ci.yaml",
"chars": 3234,
"preview": "name: CI\non:\n push:\n branches:\n - main\n pull_request:\n workflow_dispatch:\n\n# https://docs.github.com/en/actio"
},
{
"path": ".github/workflows/release.yaml",
"chars": 1536,
"preview": "name: Release\n\non:\n release:\n types: [published]\n\njobs:\n build-and-publish:\n name: Build and publish\n runs-on"
},
{
"path": ".gitignore",
"chars": 2065,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".goose/check-manifest/manifest.json",
"chars": 404,
"preview": "{\"source_ecosystem\":{\"language\":\"python\",\"version\":\"3.13\"},\"source_dependencies\":[\"check-manifest\",\"setuptools-scm==8.3."
},
{
"path": ".goose/check-manifest/requirements.txt",
"chars": 1318,
"preview": "build==1.3.0 \\\n --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \\\n --hash=sha256:71"
},
{
"path": ".goose/node/manifest.json",
"chars": 415,
"preview": "{\"source_ecosystem\":\"node\",\"source_dependencies\":[\"prettier\"],\"lock_files\":[{\"path\":\"package-lock.json\",\"checksum\":\"sha2"
},
{
"path": ".goose/node/package.json",
"chars": 54,
"preview": "{\"lockfileVersion\":3,\"dependencies\":{\"prettier\":\"*\"}}\n"
},
{
"path": ".goose/python/manifest.json",
"chars": 409,
"preview": "{\"source_ecosystem\":{\"language\":\"python\",\"version\":\"3.13\"},\"source_dependencies\":[\"blacken-docs\",\"check-jsonschema\",\"edi"
},
{
"path": ".goose/python/requirements.txt",
"chars": 41288,
"preview": "attrs==25.4.0 \\\n --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \\\n --hash=sha256:a"
},
{
"path": ".readthedocs.yml",
"chars": 299,
"preview": "# https://docs.readthedocs.io/en/stable/config-file/v2.html\nversion: 2\nsphinx:\n configuration: docs/conf.py\nbuild:\n os"
},
{
"path": "LICENSE",
"chars": 1526,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2020-2022, Anton Agestam\nAll rights reserved.\n\nRedistribution and use in source and "
},
{
"path": "MANIFEST.in",
"chars": 357,
"preview": "exclude Makefile\nexclude .editorconfig\nrecursive-exclude docs *\nrecursive-exclude .github *\nrecursive-exclude .goose *\nr"
},
{
"path": "Makefile",
"chars": 1560,
"preview": "SHELL := /usr/bin/env bash\n\n.PHONY: all\n\n# Currently running typeguard on all modules except:\n# - phantom.interval\n# - p"
},
{
"path": "README.md",
"chars": 7164,
"preview": "<p align=center><img src=https://raw.githubusercontent.com/antonagestam/phantom-types/main/docs/phantom.svg></p>\n\n<h1 al"
},
{
"path": "codecov.yml",
"chars": 33,
"preview": "comment:\n require_changes: true\n"
},
{
"path": "docs/Makefile",
"chars": 634,
"preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
},
{
"path": "docs/conf.py",
"chars": 2392,
"preview": "# This file only contains a selection of the most common options. For a full list see\n# the documentation:\n# https://www"
},
{
"path": "docs/index.rst",
"chars": 580,
"preview": "phantom-types\n=============\n\nThis is the documentation of phantom-types, a library that let's you arbitrarily narrow\nbui"
},
{
"path": "docs/pages/composing-types.rst",
"chars": 4354,
"preview": ".. _composing:\n\nComposing types\n***************\n\nBounds\n======\n\nThe bound of a phantom type is the type that its values "
},
{
"path": "docs/pages/external-wrappers.rst",
"chars": 901,
"preview": "External wrappers\n=================\n\nA collection of phantom types that wraps functionality of well maintained\nimplement"
},
{
"path": "docs/pages/functional-composition.rst",
"chars": 620,
"preview": "Functional composition\n======================\n\nWhen composing predicates for phantom types it won't take long before you"
},
{
"path": "docs/pages/getting-started.rst",
"chars": 5330,
"preview": "Getting Started\n===============\n\nCreating phantom types\n----------------------\n\nPhantom types are created by subclassing"
},
{
"path": "docs/pages/implementation.rst",
"chars": 750,
"preview": "Implementation\n==============\n\nHow are phantom types implemented?\n----------------------------------\n\nphantom-types make"
},
{
"path": "docs/pages/predicates.rst",
"chars": 985,
"preview": ".. _predicates:\n\nPredicates and factories\n========================\n\nPredicates are functions that return a boolean value"
},
{
"path": "docs/pages/pydantic-support.rst",
"chars": 1101,
"preview": "Pydantic Support\n================\n\nphantom-types supports pydantic_ out of the box by providing a\n:func:`__get_validator"
},
{
"path": "docs/pages/types.rst",
"chars": 1966,
"preview": ".. _types:\n\nTypes\n=====\n\nBase classes\n------------\n\n.. automodule:: phantom\n\n.. autoclass:: Phantom\n :members:\n :u"
},
{
"path": "docs-requirements.txt",
"chars": 29283,
"preview": "# This file was autogenerated by uv via the following command:\n# 'make docs-requirements'\naccessible-pygments==0.0.5 "
},
{
"path": "goose.yaml",
"chars": 2257,
"preview": "environments:\n - ecosystem:\n language: python\n version: \"3.13\"\n dependencies:\n - ruff\n - pre-com"
},
{
"path": "mypy.ini",
"chars": 381,
"preview": "[mypy]\npython_version = 3.10\npretty = True\nfiles = src, tests\nshow_error_codes = True\nshow_error_context = True\nshow_err"
},
{
"path": "pyproject.toml",
"chars": 2698,
"preview": "[build-system]\nrequires = [\n \"setuptools==80.9.0\",\n \"setuptools-scm==8.3.1\",\n \"wheel==0.45.1\",\n]\nbuild-backend = \"set"
},
{
"path": "ruff.toml",
"chars": 906,
"preview": "fix = true\ntarget-version = \"py310\"\n\n[lint]\nextend-select = [\n # bugbear\n \"B\",\n # comprehensions\n \"C4\",\n # mccabe\n "
},
{
"path": "setup.cfg",
"chars": 258,
"preview": "[coverage:run]\nsource = phantom\nbranch = True\n\n[coverage:report]\nskip_covered = True\nshow_missing = True\nexclude_lines ="
},
{
"path": "src/phantom/__init__.py",
"chars": 742,
"preview": "\"\"\"\nUse ``Phantom`` to create arbitrary phantom types using boolean predicates.\n\n.. code-block:: python\n\n from phanto"
},
{
"path": "src/phantom/_base.py",
"chars": 6722,
"preview": "from __future__ import annotations\n\nimport abc\nfrom collections.abc import Callable\nfrom collections.abc import Iterable"
},
{
"path": "src/phantom/_hypothesis.py",
"chars": 692,
"preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING\nfrom typing im"
},
{
"path": "src/phantom/_utils/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/phantom/_utils/misc.py",
"chars": 4888,
"preview": "# Workaround for something that looks like this bug\n# https://github.com/pytest-dev/pytest/issues/4386\nfrom __future__ i"
},
{
"path": "src/phantom/_utils/types.py",
"chars": 3215,
"preview": "from typing import Protocol\nfrom typing import TypeVar\nfrom typing import runtime_checkable\n\nfrom numerary.protocol impo"
},
{
"path": "src/phantom/boolean.py",
"chars": 1331,
"preview": "\"\"\"\nTypes describing objects that coerce to either ``True`` or ``False`` respectively when\ncalling ``bool()`` on them.\n\""
},
{
"path": "src/phantom/bounds.py",
"chars": 1587,
"preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom collections.abc import Iterable\nfrom colle"
},
{
"path": "src/phantom/datetime.py",
"chars": 3391,
"preview": "\"\"\"\nTypes for narrowing on the builtin datetime types.\n\nThese types can be used without installing any extra dependencie"
},
{
"path": "src/phantom/errors.py",
"chars": 75,
"preview": "class BoundError(TypeError): ...\n\n\nclass MissingDependency(Exception): ...\n"
},
{
"path": "src/phantom/ext/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/phantom/ext/phonenumbers.py",
"chars": 2854,
"preview": "\"\"\"\nRequires the phonenumbers_ package which can be installed with:\n\n.. _phonenumbers: https://pypi.org/project/phonenum"
},
{
"path": "src/phantom/fn.py",
"chars": 1735,
"preview": "from __future__ import annotations\n\nimport functools\nfrom collections.abc import Callable\nfrom functools import partial\n"
},
{
"path": "src/phantom/interval.py",
"chars": 10407,
"preview": "\"\"\"\nTypes for describing narrower sets of numbers than builtin numeric types like ``int``\nand ``float``. Use the provide"
},
{
"path": "src/phantom/iso3166.py",
"chars": 4857,
"preview": "\"\"\"\nExposes a :py:class:`CountryCode` type that is a union of a :py:class:`Literal`\ncontaining all ISO3166 alpha-2 codes"
},
{
"path": "src/phantom/negated.py",
"chars": 1468,
"preview": "\"\"\"\nThis module provides a single type: :py:class:`SequenceNotStr`. This type is equivalent\nto :py:class:`typing.Sequenc"
},
{
"path": "src/phantom/predicates/__init__.py",
"chars": 55,
"preview": "from ._base import Predicate\n\n__all__ = (\"Predicate\",)\n"
},
{
"path": "src/phantom/predicates/_base.py",
"chars": 210,
"preview": "from collections.abc import Callable\nfrom typing import TypeAlias\nfrom typing import TypeVar\n\nT_contra = TypeVar(\"T_cont"
},
{
"path": "src/phantom/predicates/_utils.py",
"chars": 980,
"preview": "from collections.abc import Callable\nfrom functools import partial\nfrom typing import TypeVar\n\n\ndef _explode_partial(obj"
},
{
"path": "src/phantom/predicates/boolean.py",
"chars": 2932,
"preview": "from collections.abc import Iterable\nfrom typing import Literal\nfrom typing import TypeVar\n\nfrom . import Predicate\nfrom"
},
{
"path": "src/phantom/predicates/collection.py",
"chars": 1784,
"preview": "from collections.abc import Container\nfrom collections.abc import Iterable\nfrom collections.abc import Sized\nfrom typing"
},
{
"path": "src/phantom/predicates/datetime.py",
"chars": 476,
"preview": "import datetime\n\nfrom .boolean import negate\n\n\ndef is_tz_aware(dt: datetime.datetime) -> bool:\n \"\"\"Return :py:const:`"
},
{
"path": "src/phantom/predicates/generic.py",
"chars": 1430,
"preview": "import typeguard\nfrom typeguard import CollectionCheckStrategy\nfrom typeguard import ForwardRefPolicy\n\nfrom . import Pre"
},
{
"path": "src/phantom/predicates/interval.py",
"chars": 1792,
"preview": "\"\"\"\nFunctions that create new predicates that succeed when their argument is strictly or non\nstrictly between the upper "
},
{
"path": "src/phantom/predicates/numeric.py",
"chars": 2599,
"preview": "from typing import TypeVar\n\nfrom phantom._utils.types import SupportsGe\nfrom phantom._utils.types import SupportsGt\nfrom"
},
{
"path": "src/phantom/predicates/re.py",
"chars": 755,
"preview": "from re import Pattern\n\nfrom . import Predicate\nfrom ._utils import bind_name\n\n\ndef is_match(pattern: Pattern[str]) -> P"
},
{
"path": "src/phantom/py.typed",
"chars": 0,
"preview": ""
},
{
"path": "src/phantom/re.py",
"chars": 2496,
"preview": "\"\"\"\nTypes for representing strings that match a pattern.\n\n.. code-block:: python\n\n class Greeting(Match, pattern=r\"^("
},
{
"path": "src/phantom/schema.py",
"chars": 1801,
"preview": "from collections.abc import Sequence\nfrom typing import Literal\n\nfrom typing_extensions import TypedDict\nfrom typing_ext"
},
{
"path": "src/phantom/sized.py",
"chars": 7797,
"preview": "\"\"\"\nTypes describing collections with size boundaries. These types should only be used with\nimmutable collections. There"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/ext/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/ext/test_hypothesis.py",
"chars": 3098,
"preview": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom dataclasses import fields\nfrom functools impo"
},
{
"path": "tests/ext/test_phonenumbers.py",
"chars": 4513,
"preview": "import pytest\nfrom typing_extensions import assert_type\n\nfrom phantom.errors import BoundError\nfrom phantom.ext.phonenum"
},
{
"path": "tests/ext/test_phonenumbers.yaml",
"chars": 1472,
"preview": "- case: bound_is_not_subtype\n main: |\n from phantom.ext.phonenumbers import PhoneNumber\n from phantom.ext.phonenu"
},
{
"path": "tests/predicates/__init__.py",
"chars": 72,
"preview": "import pytest\n\npytest.register_assert_rewrite(\"tests.predicates.utils\")\n"
},
{
"path": "tests/predicates/test_boolean.py",
"chars": 9056,
"preview": "from collections.abc import Iterable\n\nimport pytest\n\nfrom phantom import Predicate\nfrom phantom.predicates import boolea"
},
{
"path": "tests/predicates/test_collection.py",
"chars": 5008,
"preview": "from collections.abc import Container\nfrom collections.abc import Iterable\nfrom collections.abc import Sized\n\nimport pyt"
},
{
"path": "tests/predicates/test_datetime.py",
"chars": 969,
"preview": "import datetime\n\nimport pytest\n\nfrom phantom.predicates.datetime import is_tz_aware\nfrom phantom.predicates.datetime imp"
},
{
"path": "tests/predicates/test_generic.py",
"chars": 2165,
"preview": "import pytest\n\nfrom phantom.predicates import generic\n\nfrom .utils import assert_predicate_name_equals\n\n\nclass TestEqual"
},
{
"path": "tests/predicates/test_interval.py",
"chars": 3779,
"preview": "from typing import TypeAlias\n\nimport pytest\n\nfrom phantom.predicates import interval\n\nfrom .utils import assert_predicat"
},
{
"path": "tests/predicates/test_numeric.py",
"chars": 4406,
"preview": "import pytest\n\nfrom phantom.predicates import numeric\n\nfrom .utils import assert_predicate_name_equals\n\n\nclass TestLess:"
},
{
"path": "tests/predicates/test_re.py",
"chars": 1178,
"preview": "import re\n\nfrom phantom.predicates.re import is_full_match\nfrom phantom.predicates.re import is_match\n\nfrom .utils impor"
},
{
"path": "tests/predicates/test_utils.py",
"chars": 518,
"preview": "from functools import partial\n\nfrom phantom.predicates import boolean\n\nfrom .utils import assert_predicate_name_equals\n\n"
},
{
"path": "tests/predicates/utils.py",
"chars": 286,
"preview": "from phantom import Predicate\n\n\ndef assert_predicate_name_equals(predicate: Predicate, expected_name: str) -> None:\n "
},
{
"path": "tests/pydantic/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/pydantic/test_datetime.py",
"chars": 1369,
"preview": "import datetime\n\nimport pydantic\nimport pytest\nfrom pydantic import ValidationError\n\nfrom phantom.datetime import TZAwar"
},
{
"path": "tests/pydantic/test_schemas.py",
"chars": 7807,
"preview": "import pydantic\nimport pytest\n\nfrom phantom.datetime import TZAware\nfrom phantom.datetime import TZNaive\nfrom phantom.ex"
},
{
"path": "tests/test_base.py",
"chars": 5464,
"preview": "import sys\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Union\n\nimport pytes"
},
{
"path": "tests/test_boolean.py",
"chars": 1247,
"preview": "import pytest\n\nfrom phantom.boolean import Falsy\nfrom phantom.boolean import Truthy\n\nparametrize_truthy = pytest.mark.pa"
},
{
"path": "tests/test_datetime.py",
"chars": 5808,
"preview": "import datetime\n\nimport pytest\n\nfrom phantom import BoundError\nfrom phantom.datetime import TZAware\nfrom phantom.datetim"
},
{
"path": "tests/test_datetime.yaml",
"chars": 1302,
"preview": "- case: calling_function_with_unknown_raises\n main: |\n import datetime\n from phantom.datetime import TZAware\n\n "
},
{
"path": "tests/test_fn.py",
"chars": 3560,
"preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom collections.abc import Sequence\nfrom funct"
},
{
"path": "tests/test_fn.yaml",
"chars": 1964,
"preview": "- case: test_compose2_can_infer_types\n disable_cache: true\n regex: true\n main: |\n from typing import Callable\n "
},
{
"path": "tests/test_intersection.yaml",
"chars": 358,
"preview": "- case: asserting_twice_results_in_intersection_type\n main: |\n from phantom.interval import Portion, Exclusive\n\n "
},
{
"path": "tests/test_interval.py",
"chars": 7369,
"preview": "from __future__ import annotations\n\nfrom decimal import Decimal\nfrom functools import total_ordering\n\nimport pytest\n\nfro"
},
{
"path": "tests/test_interval.yaml",
"chars": 1605,
"preview": "- case: calling_function_with_unknown_raises\n main: |\n from phantom.interval import Natural\n\n def take_nat(a: Nat"
},
{
"path": "tests/test_iso3166.py",
"chars": 1576,
"preview": "import pytest\n\nfrom phantom.iso3166 import InvalidCountryCode\nfrom phantom.iso3166 import ParsedAlpha2\nfrom phantom.iso3"
},
{
"path": "tests/test_iso3166.yaml",
"chars": 2402,
"preview": "- case: can_instantiate\n main: |\n from phantom.iso3166 import Alpha2, ParsedAlpha2\n\n def takes_country_code(a: Al"
},
{
"path": "tests/test_negated.py",
"chars": 1333,
"preview": "from typing import get_args\nfrom typing import get_origin\n\nimport pytest\n\nfrom phantom.negated import SequenceNotStr\n\npa"
},
{
"path": "tests/test_negated.yaml",
"chars": 1209,
"preview": "- case: test_subscripted\n main: |\n from phantom.negated import SequenceNotStr\n\n def greet(names: SequenceNotStr[s"
},
{
"path": "tests/test_re.py",
"chars": 2042,
"preview": "from __future__ import annotations\n\nimport re\n\nimport pytest\n\nfrom phantom.re import FullMatch\nfrom phantom.re import Ma"
},
{
"path": "tests/test_re.yaml",
"chars": 1695,
"preview": "- case: str_is_not_subtype_of_match\n main: |\n import re\n from phantom.re import Match\n\n class A(Match, pattern"
},
{
"path": "tests/test_sized.py",
"chars": 7423,
"preview": "from dataclasses import dataclass\nfrom typing import Final\nfrom typing import Generic\nfrom typing import TypeVar\nfrom ty"
},
{
"path": "tests/test_sized.yaml",
"chars": 1088,
"preview": "- case: test_subtype\n main: |\n from phantom.sized import NonEmpty\n from typing import Tuple\n\n class AtLeastOne"
},
{
"path": "tests/test_utils.py",
"chars": 1893,
"preview": "from typing import Union\n\nimport pytest\n\nfrom phantom._utils.misc import BoundType\nfrom phantom._utils.misc import is_su"
},
{
"path": "tests/types.py",
"chars": 662,
"preview": "from phantom.interval import Exclusive\nfrom phantom.interval import ExclusiveInclusive\nfrom phantom.interval import Incl"
},
{
"path": "typing-requirements.txt",
"chars": 11379,
"preview": "# This file was autogenerated by uv via the following command:\n# 'make typing-requirements'\nbeartype==0.22.5 \\\n --"
}
]
About this extraction
This page contains the full source code of the antonagestam/phantom-types GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 104 files (293.5 KB), approximately 98.7k tokens, and a symbol index with 614 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.