├── .github └── workflows │ ├── docs.yaml.bkp │ ├── package.yaml.bkp │ ├── pycqa.yaml │ └── sast.yaml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── docs ├── Makefile ├── api │ ├── between.md │ ├── between.rst │ ├── card.md │ ├── card.rst │ ├── country.md │ ├── country.rst │ ├── cron.md │ ├── cron.rst │ ├── crypto_addresses.md │ ├── crypto_addresses.rst │ ├── domain.md │ ├── domain.rst │ ├── email.md │ ├── email.rst │ ├── encoding.md │ ├── encoding.rst │ ├── finance.md │ ├── finance.rst │ ├── hashes.md │ ├── hashes.rst │ ├── hostname.md │ ├── hostname.rst │ ├── i18n.md │ ├── i18n.rst │ ├── iban.md │ ├── iban.rst │ ├── ip_address.md │ ├── ip_address.rst │ ├── length.md │ ├── length.rst │ ├── mac_address.md │ ├── mac_address.rst │ ├── slug.md │ ├── slug.rst │ ├── url.md │ ├── url.rst │ ├── utils.md │ ├── utils.rst │ ├── uuid.md │ └── uuid.rst ├── assets │ └── icons │ │ └── favicon.svg ├── conf.py ├── index.md ├── index.rst ├── install_and_use.md ├── install_and_use.rst └── make.bat ├── mkdocs.yaml ├── package ├── export │ ├── __init__.py │ └── __main__.py ├── requirements.sphinx.txt ├── requirements.testing.txt ├── requirements.tooling.txt ├── roll.ps1 └── roll.sh ├── pdm.lock ├── pyproject.toml ├── src ├── __init__.py └── validators │ ├── __init__.py │ ├── _extremes.py │ ├── _tld.txt │ ├── between.py │ ├── card.py │ ├── country.py │ ├── cron.py │ ├── crypto_addresses │ ├── __init__.py │ ├── bsc_address.py │ ├── btc_address.py │ ├── eth_address.py │ └── trx_address.py │ ├── domain.py │ ├── email.py │ ├── encoding.py │ ├── finance.py │ ├── hashes.py │ ├── hostname.py │ ├── i18n │ ├── __init__.py │ ├── es.py │ ├── fi.py │ ├── fr.py │ ├── ind.py │ └── ru.py │ ├── iban.py │ ├── ip_address.py │ ├── length.py │ ├── mac_address.py │ ├── py.typed │ ├── slug.py │ ├── uri.py │ ├── url.py │ ├── utils.py │ └── uuid.py └── tests ├── crypto_addresses ├── test_bsc_address.py ├── test_btc_address.py ├── test_eth_address.py └── test_trx_address.py ├── i18n ├── test_es.py ├── test_fi.py ├── test_fr.py ├── test_ind.py └── test_ru.py ├── test__extremes.py ├── test_between.py ├── test_card.py ├── test_country.py ├── test_cron.py ├── test_domain.py ├── test_email.py ├── test_encoding.py ├── test_finance.py ├── test_hashes.py ├── test_hostname.py ├── test_iban.py ├── test_ip_address.py ├── test_length.py ├── test_mac_address.py ├── test_slug.py ├── test_url.py ├── test_uuid.py └── test_validation_failure.py /.github/workflows/docs.yaml.bkp: -------------------------------------------------------------------------------- 1 | # Documentation 2 | name: docs 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["master"] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | env: 13 | BUILD_PATH: . 14 | PIP_DISABLE_PIP_VERSION_CHECK: 1 15 | steps: 16 | # checkout repository 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | # set up specific python version 20 | - name: Set up Python v3.12 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.12" 24 | # building 25 | - name: Install 'documentation' dependencies 26 | run: | 27 | pip install . 28 | pip install -r package/requirements.mkdocs.txt 29 | - name: Build documentation 30 | run: python package/export doc 31 | # set up Pages 32 | - name: Set up Pages 33 | uses: actions/configure-pages@v4 34 | # upload static page 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: ${{ env.BUILD_PATH }}/site 39 | deploy: 40 | needs: build 41 | runs-on: ubuntu-latest 42 | permissions: 43 | pages: write 44 | id-token: write 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | # deploy documentation 49 | name: Deploy 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /.github/workflows/package.yaml.bkp: -------------------------------------------------------------------------------- 1 | # Publish to PyPI 2 | name: package 3 | on: 4 | workflow_dispatch: 5 | # uncomment the following lines to enable workflow on package release 6 | # release: 7 | # types: [published] 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | env: 14 | BUILD_PATH: . 15 | PIP_DISABLE_PIP_VERSION_CHECK: 1 16 | steps: 17 | # checkout repository 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | # set up specific python version 21 | - name: Set up Python v3.8 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.8" 25 | # install dependencies 26 | - name: Install dependencies 27 | run: | 28 | python -m venv .venv 29 | ./.venv/bin/python -m pip install --upgrade pip 30 | ./.venv/bin/pip install . 31 | ./.venv/bin/pip install -r package/requirements.sphinx.txt 32 | ./.venv/bin/pip install build 33 | # build package 34 | - name: Build package 35 | run: | 36 | . ./.venv/bin/activate 37 | python package/export pkg 38 | deactivate 39 | # upload package as artifact 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: ${{ env.BUILD_PATH }}/dist 44 | publish: 45 | needs: build 46 | runs-on: ubuntu-latest 47 | permissions: 48 | id-token: write 49 | steps: 50 | # publish package 51 | - name: Publish to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | with: 54 | password: ${{ secrets.PYPI_API_TOKEN }} 55 | -------------------------------------------------------------------------------- /.github/workflows/pycqa.yaml: -------------------------------------------------------------------------------- 1 | # Python Code Quality Analysis 2 | name: pycqa 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | jobs: 10 | tooling: 11 | runs-on: ubuntu-latest 12 | env: 13 | PIP_DISABLE_PIP_VERSION_CHECK: 1 14 | steps: 15 | # checkout repository 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | # set up specific python version 19 | - name: Set up Python v3.9 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.9" 23 | # tooling 24 | - name: Install 'tooling' dependencies 25 | run: pip install -r package/requirements.tooling.txt 26 | - name: Tooling 27 | run: | 28 | ruff format . 29 | ruff check . 30 | pyright . 31 | testing: 32 | strategy: 33 | fail-fast: true 34 | matrix: 35 | os: [ubuntu-latest, macos-latest, windows-latest] 36 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 37 | runs-on: ${{ matrix.os }} 38 | steps: 39 | # checkout repository again! 40 | # ref: https://github.com/actions/checkout/issues/19 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | # set up specific python version 44 | - name: Set up Python v${{ matrix.python-version }} 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | cache: "pip" 49 | # testing 50 | - name: Install 'testing' dependencies 51 | run: | 52 | pip install -r package/requirements.testing.txt 53 | pip install . 54 | - name: Testing 55 | run: pytest . 56 | -------------------------------------------------------------------------------- /.github/workflows/sast.yaml: -------------------------------------------------------------------------------- 1 | # Static Application Security Testing 2 | name: sast 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["master"] 7 | pull_request: 8 | branches: ["master"] 9 | schedule: 10 | - cron: "00 00 * * 0" 11 | jobs: 12 | sast: 13 | permissions: 14 | contents: read # for actions/checkout to fetch code 15 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 16 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Bandit 21 | uses: mdegis/bandit-action@85fcc340c3b0bf5d86029abb49b9aac916d807b2 22 | with: 23 | # exit with 0, even with results found 24 | # exit_zero: true # optional, default is DEFAULT 25 | # Github token of the repository (automatically created by Github) 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information. 27 | # File or directory to run bandit on 28 | path: ./src/validators # optional, default is . 29 | # Report only issues of a given severity level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything) 30 | # level: # optional, default is UNDEFINED 31 | # Report only issues of a given confidence level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything) 32 | # confidence: # optional, default is UNDEFINED 33 | # comma-separated list of paths (glob patterns supported) to exclude from scan (note that these are in addition to the excluded paths provided in the config file) (default: .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg) 34 | excluded_paths: .github,.pytest_cache,.venv,.vscode,site,tests # optional, default is DEFAULT 35 | # comma-separated list of test IDs to skip 36 | # skips: # optional, default is DEFAULT 37 | # path to a .bandit file that supplies command line arguments 38 | # ini_path: # optional, default is DEFAULT 39 | # https://github.com/marketplace/actions/bandit-scan is ISC licensed, by abirismyname 40 | # https://pypi.org/project/bandit/ is Apache v2.0 licensed, by PyCQA 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/*.1 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | .venv.dev/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | site/ 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | .idea/ 165 | 166 | # VSCode 167 | .vscode/ 168 | 169 | # asdf 170 | .tool-versions 171 | 172 | # rtx/mise 173 | .rtx.toml 174 | .mise.toml 175 | mise.toml 176 | 177 | # ruff 178 | .ruff_cache 179 | 180 | # taplo 181 | .taplo.toml 182 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `validators` 2 | 3 | Hi, to start, you need the following installed on your system. 4 | 5 | 1. [Git](https://git-scm.com) 6 | 2. [Python](https://www.python.org) v3.8 or later 7 | 3. [PDM](https://pdm-project.org) for easy dependency management 8 | 4. (Optional/Recommended) [`NodeJS`](https://nodejs.org/en) for type checking 9 | 5. (Optional/Recommended) [`mise`](https://github.com/jdx/mise) to manage multiple versions of Python, NodeJS and other such tools. 10 | 11 | First [fork this repository](https://github.com/python-validators/validators/fork). (If you are or intend to be a collaborator, uncheck "fork only `master`", because for versioned docs you'll need `gh-pages` branch too.) Clone it to your system and install the development dependencies. 12 | 13 | ```sh 14 | # clone repository 15 | $ git clone "https://github.com/YOUR-USERNAME/validators.git" 16 | # change directory 17 | $ cd validators 18 | # install development dependencies 19 | $ pdm install 20 | ``` 21 | 22 | Activate the virtual environment and run `tox` to verify test cases. 23 | 24 | ```sh 25 | # activate virtual environment 26 | $ . ./.venv/bin/activate # replace `/bin/` with `/Scripts/` if you're on Windows. 27 | # run tox for linting, type checking, formatting etc. 28 | $ tox 29 | ``` 30 | 31 | Create a git branch and make changes to the source code. If needed, test your changes by running `pytest`. Execute `tox` to format, lint and type-check your code before committing. Commit, push and create a pull request. If you're in doubt, feel free to start a discussion [here](https://github.com/python-validators/validators/discussions). 32 | 33 | ## Documentation 34 | 35 | > Documentation is extracted from the source code. Please follow [Google's Python Doc Style](https://google.github.io/styleguide/pyguide.html). 36 | 37 | If you're adding/removing a module, you must update the `nav` key in `mkdocs.yml`. 38 | Then export documentation as follows: 39 | 40 | ```sh 41 | $ . ./.venv/bin/activate 42 | # generate documentation 43 | $ python package/export TYPE # where TYPE is any of `doc`, `man` or `web`. 44 | 45 | # doc - generates docs found here: https://yozachar.github.io/pyvalidators 46 | # man - generates sphinx based manpages 47 | # web - generates sphinx based web docs 48 | ``` 49 | 50 | You can preview the generated documentation: 51 | 52 | ```sh 53 | $ . ./.venv/bin/activate 54 | # To preview mkdocs 55 | $ mkdocs serve 56 | # To preview man pages 57 | $ man docs/_build/man/validators.1 58 | # To preview sphinx webpages 59 | $ python -m http.server -d docs/_build/web 60 | ``` 61 | 62 | ## Versioning, Packaging & Releasing (for collaborators) 63 | 64 | > You must be familiar with [semantic versioning](https://semver.org) and [Python packaging](https://packaging.python.org). 65 | 66 | ### Tagging 67 | 68 | 1. Take a look at [`CHANGES.md`](CHANGES.md). They are generated with [GitHub's releaser](https://github.com/python-validators/validators/releases/new), and then modified to fit the shown style. 69 | 2. Update the [changelog](CHANGES.md). Version number must be updated in both [`SECURITY.md`](SECURITY.md) and [`src/validators/__init__.py`](src/validators/__init__.py). 70 | 3. The final merge commit on the upstream (i.e. this repo) is tagged. 71 | 72 | ```sh 73 | # syncing with upstream 74 | $ git pull upstream master 75 | $ git push 76 | # tagging that final merge commit before release 77 | $ GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "vMAJOR.MINOR.PATCH" MAJOR.MINOR.PATCH 78 | # pushing tag to remote 79 | $ git push --tag 80 | $ git push upstream --tag 81 | ``` 82 | 83 | ### Versioned documentation 84 | 85 | 1. To preview versioned docs, run `mike serve` (`mike` is a dev dependency). 86 | 2. Then (look at ) 87 | - to publish stable docs run `mike deploy -p -u VERSION stable` after checking out to a stable tag name like `0.28.3` (note: document `VERSION = 0.29 if tag_name == 0.29.1`). 88 | - to publish bleeding-edge docs run `mike deploy -p -u dev master` after checking out to the `master` branch. 89 | 3. This will deploy docs to the `gh-pages` branch (see: ) 90 | 91 | ### Packaging and releasing 92 | 93 | 1. Run `./package/roll.sh` (or `./package/roll.ps1`) to generate both `sdist` and `bdist`. 94 | 2. Install [`twine`](https://pypi.org/project/twine) using [`pipx`](https://pipx.pypa.io) to upload package to PyPI. 95 | 96 | ```sh 97 | # publishing 98 | $ twine check dist/* 99 | $ twine upload dist/* 100 | ``` 101 | 102 | 3. Create a GitHub release with the contents from the [changelog](CHANGES.md). Upload the wheel from `dist/` along with the shasum file generated with: 103 | 104 | ```sh 105 | # generate sha256sum 106 | $ sha256sum dist/validators-VERSION-py3-none-any.whl > dist/validators-VERSION-py3-none-any.whl.sha256 107 | ``` 108 | 109 | --- 110 | 111 | Thank your for taking interest in this library! 112 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 - 2025 Konsta Vesterinen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # MANIFEST.in stays as long as pyproject.toml does not officially support it 2 | # https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ 3 | 4 | # include 5 | include CHANGES.md 6 | include mkdocs.yaml 7 | 8 | 9 | # global-include 10 | 11 | 12 | # recursive-include 13 | recursive-include tests * 14 | recursive-include docs * 15 | 16 | 17 | # graft 18 | 19 | 20 | # exclude 21 | 22 | 23 | # global-exclude 24 | 25 | 26 | # recursive-exclude 27 | 28 | 29 | # prune 30 | prune docs/_build 31 | prune **/__pycache__ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # validators - Python Data Validation for Humans™ 2 | 3 | [![PyCQA][pycqa-badge]][pycqa-link] [![SAST][sast-badge]][sast-link] [![Docs][docs-badge]][docs-link] [![Version][vs-badge]][vs-link] [![Downloads][dw-badge]][dw-link] 4 | 5 | 6 | 7 | Python has all kinds of data validation tools, but every one of them seems to 8 | require defining a schema or form. I wanted to create a simple validation 9 | library where validating a simple value does not require defining a form or a 10 | schema. 11 | 12 | ```shell 13 | pip install validators 14 | ``` 15 | 16 | Then, 17 | 18 | ```python 19 | >>> import validators 20 | >>> 21 | >>> validators.email('someone@example.com') 22 | True 23 | ``` 24 | 25 | ## Resources 26 | 27 | 28 | 29 | 30 | - [Documentation](https://yozachar.github.io/pyvalidators) 31 | - [Bugtracker](https://github.com/python-validators/validators/issues) 32 | - [Security](https://github.com/python-validators/validators/blob/master/SECURITY.md) 33 | - [Code](https://github.com/python-validators/validators/) 34 | 35 | 36 | 37 | --- 38 | 39 | > **_Python 3.9 [reaches EOL in](https://endoflife.date/python) October 2025._** 40 | 41 | 42 | [sast-badge]: https://github.com/python-validators/validators/actions/workflows/sast.yaml/badge.svg 43 | [sast-link]: https://github.com/python-validators/validators/actions/workflows/sast.yaml 44 | [pycqa-badge]: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml/badge.svg 45 | [pycqa-link]: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml 46 | [docs-badge]: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment/badge.svg 47 | [docs-link]: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment 48 | [vs-badge]: https://img.shields.io/pypi/v/validators?logo=pypi&logoColor=white&label=version&color=blue 49 | [vs-link]: https://pypi.python.org/pypi/validators/ 50 | [dw-badge]: https://img.shields.io/pypi/dm/validators?logo=pypi&logoColor=white&color=blue 51 | [dw-link]: https://pypi.python.org/pypi/validators/ 52 | 53 | 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ---------- | ------------------ | 7 | | `>=0.35.0` | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Please read [CVD](https://resources.sei.cmu.edu/asset_files/SpecialReport/2017_003_001_503340.pdf) before reporting vulnerabilities. 12 | 13 | - We do our best to write safe code. 14 | - [@kvesteri](https://github.com/kvesteri) is the author of `validators`. 15 | - You can raise security concerns [here](https://github.com/python-validators/validators/discussions/categories/security). 16 | - None of us can promise any response time-frame, but we'll try. 17 | 18 | That said, use the package at your own risk. The source code is open, we encourage you to read. _Spammers will be banned._ 19 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api/between.md: -------------------------------------------------------------------------------- 1 | # between 2 | 3 | ::: validators.between.between 4 | -------------------------------------------------------------------------------- /docs/api/between.rst: -------------------------------------------------------------------------------- 1 | between 2 | ------- 3 | 4 | .. module:: validators.between 5 | .. autofunction:: between 6 | -------------------------------------------------------------------------------- /docs/api/card.md: -------------------------------------------------------------------------------- 1 | # card 2 | 3 | ::: validators.card.amex 4 | ::: validators.card.card_number 5 | ::: validators.card.diners 6 | ::: validators.card.discover 7 | ::: validators.card.jcb 8 | ::: validators.card.mastercard 9 | ::: validators.card.mir 10 | ::: validators.card.unionpay 11 | ::: validators.card.visa 12 | -------------------------------------------------------------------------------- /docs/api/card.rst: -------------------------------------------------------------------------------- 1 | card 2 | ---- 3 | 4 | .. module:: validators.card 5 | .. autofunction:: amex 6 | .. autofunction:: card_number 7 | .. autofunction:: diners 8 | .. autofunction:: discover 9 | .. autofunction:: jcb 10 | .. autofunction:: mastercard 11 | .. autofunction:: mir 12 | .. autofunction:: unionpay 13 | .. autofunction:: visa 14 | -------------------------------------------------------------------------------- /docs/api/country.md: -------------------------------------------------------------------------------- 1 | # country 2 | 3 | ::: validators.country.calling_code 4 | ::: validators.country.country_code 5 | ::: validators.country.currency 6 | -------------------------------------------------------------------------------- /docs/api/country.rst: -------------------------------------------------------------------------------- 1 | country 2 | ------- 3 | 4 | .. module:: validators.country 5 | .. autofunction:: calling_code 6 | .. autofunction:: country_code 7 | .. autofunction:: currency 8 | -------------------------------------------------------------------------------- /docs/api/cron.md: -------------------------------------------------------------------------------- 1 | # cron 2 | 3 | ::: validators.cron.cron 4 | -------------------------------------------------------------------------------- /docs/api/cron.rst: -------------------------------------------------------------------------------- 1 | cron 2 | ---- 3 | 4 | .. module:: validators.cron 5 | .. autofunction:: cron 6 | -------------------------------------------------------------------------------- /docs/api/crypto_addresses.md: -------------------------------------------------------------------------------- 1 | # crypto_addresses 2 | 3 | ::: validators.crypto_addresses.bsc_address 4 | ::: validators.crypto_addresses.btc_address 5 | ::: validators.crypto_addresses.eth_address 6 | ::: validators.crypto_addresses.trx_address 7 | -------------------------------------------------------------------------------- /docs/api/crypto_addresses.rst: -------------------------------------------------------------------------------- 1 | crypto_addresses 2 | ---------------- 3 | 4 | .. module:: validators.crypto_addresses 5 | .. autofunction:: bsc_address 6 | .. autofunction:: btc_address 7 | .. autofunction:: eth_address 8 | .. autofunction:: trx_address 9 | -------------------------------------------------------------------------------- /docs/api/domain.md: -------------------------------------------------------------------------------- 1 | # domain 2 | 3 | ::: validators.domain.domain 4 | -------------------------------------------------------------------------------- /docs/api/domain.rst: -------------------------------------------------------------------------------- 1 | domain 2 | ------ 3 | 4 | .. module:: validators.domain 5 | .. autofunction:: domain 6 | -------------------------------------------------------------------------------- /docs/api/email.md: -------------------------------------------------------------------------------- 1 | # email 2 | 3 | ::: validators.email.email 4 | -------------------------------------------------------------------------------- /docs/api/email.rst: -------------------------------------------------------------------------------- 1 | email 2 | ----- 3 | 4 | .. module:: validators.email 5 | .. autofunction:: email 6 | -------------------------------------------------------------------------------- /docs/api/encoding.md: -------------------------------------------------------------------------------- 1 | # encoding 2 | 3 | ::: validators.encoding.base16 4 | ::: validators.encoding.base32 5 | ::: validators.encoding.base58 6 | ::: validators.encoding.base64 7 | -------------------------------------------------------------------------------- /docs/api/encoding.rst: -------------------------------------------------------------------------------- 1 | encoding 2 | -------- 3 | 4 | .. module:: validators.encoding 5 | .. autofunction:: base16 6 | .. autofunction:: base32 7 | .. autofunction:: base58 8 | .. autofunction:: base64 9 | -------------------------------------------------------------------------------- /docs/api/finance.md: -------------------------------------------------------------------------------- 1 | # finance 2 | 3 | ::: validators.finance.cusip 4 | ::: validators.finance.isin 5 | ::: validators.finance.sedol 6 | -------------------------------------------------------------------------------- /docs/api/finance.rst: -------------------------------------------------------------------------------- 1 | finance 2 | ------- 3 | 4 | .. module:: validators.finance 5 | .. autofunction:: cusip 6 | .. autofunction:: isin 7 | .. autofunction:: sedol 8 | -------------------------------------------------------------------------------- /docs/api/hashes.md: -------------------------------------------------------------------------------- 1 | # hashes 2 | 3 | ::: validators.hashes.md5 4 | ::: validators.hashes.sha1 5 | ::: validators.hashes.sha224 6 | ::: validators.hashes.sha256 7 | ::: validators.hashes.sha384 8 | ::: validators.hashes.sha512 9 | -------------------------------------------------------------------------------- /docs/api/hashes.rst: -------------------------------------------------------------------------------- 1 | hashes 2 | ------ 3 | 4 | .. module:: validators.hashes 5 | .. autofunction:: md5 6 | .. autofunction:: sha1 7 | .. autofunction:: sha224 8 | .. autofunction:: sha256 9 | .. autofunction:: sha384 10 | .. autofunction:: sha512 11 | -------------------------------------------------------------------------------- /docs/api/hostname.md: -------------------------------------------------------------------------------- 1 | # hostname 2 | 3 | ::: validators.hostname.hostname 4 | -------------------------------------------------------------------------------- /docs/api/hostname.rst: -------------------------------------------------------------------------------- 1 | hostname 2 | -------- 3 | 4 | .. module:: validators.hostname 5 | .. autofunction:: hostname 6 | -------------------------------------------------------------------------------- /docs/api/i18n.md: -------------------------------------------------------------------------------- 1 | # i18n 2 | 3 | ::: validators.i18n.es_cif 4 | ::: validators.i18n.es_doi 5 | ::: validators.i18n.es_nie 6 | ::: validators.i18n.es_nif 7 | ::: validators.i18n.fi_business_id 8 | ::: validators.i18n.fi_ssn 9 | ::: validators.i18n.fr_department 10 | ::: validators.i18n.fr_ssn 11 | ::: validators.i18n.ind_aadhar 12 | ::: validators.i18n.ind_pan 13 | ::: validators.i18n.ru_inn 14 | -------------------------------------------------------------------------------- /docs/api/i18n.rst: -------------------------------------------------------------------------------- 1 | i18n 2 | ---- 3 | 4 | .. module:: validators.i18n 5 | .. autofunction:: es_cif 6 | .. autofunction:: es_doi 7 | .. autofunction:: es_nie 8 | .. autofunction:: es_nif 9 | .. autofunction:: fi_business_id 10 | .. autofunction:: fi_ssn 11 | .. autofunction:: fr_department 12 | .. autofunction:: fr_ssn 13 | .. autofunction:: ind_aadhar 14 | .. autofunction:: ind_pan 15 | .. autofunction:: ru_inn 16 | -------------------------------------------------------------------------------- /docs/api/iban.md: -------------------------------------------------------------------------------- 1 | # iban 2 | 3 | ::: validators.iban.iban 4 | -------------------------------------------------------------------------------- /docs/api/iban.rst: -------------------------------------------------------------------------------- 1 | iban 2 | ---- 3 | 4 | .. module:: validators.iban 5 | .. autofunction:: iban 6 | -------------------------------------------------------------------------------- /docs/api/ip_address.md: -------------------------------------------------------------------------------- 1 | # ip_address 2 | 3 | ::: validators.ip_address.ipv4 4 | ::: validators.ip_address.ipv6 5 | -------------------------------------------------------------------------------- /docs/api/ip_address.rst: -------------------------------------------------------------------------------- 1 | ip_address 2 | ---------- 3 | 4 | .. module:: validators.ip_address 5 | .. autofunction:: ipv4 6 | .. autofunction:: ipv6 7 | -------------------------------------------------------------------------------- /docs/api/length.md: -------------------------------------------------------------------------------- 1 | # length 2 | 3 | ::: validators.length.length 4 | -------------------------------------------------------------------------------- /docs/api/length.rst: -------------------------------------------------------------------------------- 1 | length 2 | ------ 3 | 4 | .. module:: validators.length 5 | .. autofunction:: length 6 | -------------------------------------------------------------------------------- /docs/api/mac_address.md: -------------------------------------------------------------------------------- 1 | # mac_address 2 | 3 | ::: validators.mac_address.mac_address 4 | -------------------------------------------------------------------------------- /docs/api/mac_address.rst: -------------------------------------------------------------------------------- 1 | mac_address 2 | ----------- 3 | 4 | .. module:: validators.mac_address 5 | .. autofunction:: mac_address 6 | -------------------------------------------------------------------------------- /docs/api/slug.md: -------------------------------------------------------------------------------- 1 | # slug 2 | 3 | ::: validators.slug.slug 4 | -------------------------------------------------------------------------------- /docs/api/slug.rst: -------------------------------------------------------------------------------- 1 | slug 2 | ---- 3 | 4 | .. module:: validators.slug 5 | .. autofunction:: slug 6 | -------------------------------------------------------------------------------- /docs/api/url.md: -------------------------------------------------------------------------------- 1 | # url 2 | 3 | ::: validators.url.url 4 | -------------------------------------------------------------------------------- /docs/api/url.rst: -------------------------------------------------------------------------------- 1 | url 2 | --- 3 | 4 | .. module:: validators.url 5 | .. autofunction:: url 6 | -------------------------------------------------------------------------------- /docs/api/utils.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | ::: validators.utils.ValidationError 4 | ::: validators.utils.validator 5 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | utils 2 | ----- 3 | 4 | .. module:: validators.utils 5 | .. autofunction:: ValidationError 6 | .. autofunction:: validator 7 | -------------------------------------------------------------------------------- /docs/api/uuid.md: -------------------------------------------------------------------------------- 1 | # uuid 2 | 3 | ::: validators.uuid.uuid 4 | -------------------------------------------------------------------------------- /docs/api/uuid.rst: -------------------------------------------------------------------------------- 1 | uuid 2 | ---- 3 | 4 | .. module:: validators.uuid 5 | .. autofunction:: uuid 6 | -------------------------------------------------------------------------------- /docs/assets/icons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder. 2 | 3 | For the full list of built-in configuration values, see the documentation: 4 | https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | """ 6 | 7 | # standard 8 | from datetime import datetime 9 | from importlib.metadata import metadata 10 | 11 | # -- Project information ---------------------------------------------------------- 12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 13 | _metadata = metadata("validators") 14 | 15 | _author_name = _metadata["author-email"].split(" <")[0] 16 | # _author_email = _metadata["author-email"].split(" <")[1].rstrip(">") 17 | 18 | project: str = _metadata["name"] 19 | author: str = _author_name 20 | project_copyright = f"2013 - {datetime.now().year}, {_author_name}" 21 | version: str = _metadata["version"] 22 | release = version 23 | 24 | # -- General configuration --------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 26 | extensions = [ 27 | "sphinx.ext.autodoc", 28 | "sphinx.ext.napoleon", 29 | "myst_parser", 30 | ] 31 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/*.md", "*.md"] 32 | 33 | 34 | # -- Options for HTML output ------------------------------------------------- 35 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 36 | html_theme = "furo" 37 | html_favicon = "./assets/icons/favicon.svg" 38 | 39 | # -- Options for manpage generation ------------------------------------------------- 40 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-man_pages 41 | man_pages = [("index", project, _metadata["summary"], [author], 1)] 42 | 43 | # -- Options for docstring parsing ------------------------------------------------- 44 | # https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html 45 | napoleon_numpy_docstring = False 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # validators - Python Data Validation for Humans™ 2 | 3 | [![PyCQA][pycqa-badge]][pycqa-link] [![SAST][sast-badge]][sast-link] [![Docs][docs-badge]][docs-link] [![Version][vs-badge]][vs-link] [![Downloads][dw-badge]][dw-link] 4 | 5 | 6 | 7 | Python has all kinds of data validation tools, but every one of them seems to 8 | require defining a schema or form. I wanted to create a simple validation 9 | library where validating a simple value does not require defining a form or a 10 | schema. 11 | 12 | ```shell 13 | pip install validators 14 | ``` 15 | 16 | Then, 17 | 18 | ```python 19 | >>> import validators 20 | >>> 21 | >>> validators.email('someone@example.com') 22 | True 23 | ``` 24 | 25 | ## Resources 26 | 27 | 28 | 29 | 30 | - [Documentation](https://yozachar.github.io/pyvalidators) 31 | - [Bugtracker](https://github.com/python-validators/validators/issues) 32 | - [Security](https://github.com/python-validators/validators/blob/master/SECURITY.md) 33 | - [Code](https://github.com/python-validators/validators/) 34 | 35 | 36 | 37 | --- 38 | 39 | > **_Python 3.9 [reaches EOL in](https://endoflife.date/python) October 2025._** 40 | 41 | 42 | [sast-badge]: https://github.com/python-validators/validators/actions/workflows/sast.yaml/badge.svg 43 | [sast-link]: https://github.com/python-validators/validators/actions/workflows/sast.yaml 44 | [pycqa-badge]: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml/badge.svg 45 | [pycqa-link]: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml 46 | [docs-badge]: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment/badge.svg 47 | [docs-link]: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment 48 | [vs-badge]: https://img.shields.io/pypi/v/validators?logo=pypi&logoColor=white&label=version&color=blue 49 | [vs-link]: https://pypi.python.org/pypi/validators/ 50 | [dw-badge]: https://img.shields.io/pypi/dm/validators?logo=pypi&logoColor=white&color=blue 51 | [dw-link]: https://pypi.python.org/pypi/validators/ 52 | 53 | 55 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | validators - Python Data Validation for Humans™ 2 | =============================================== 3 | 4 | |PyCQA| |SAST| |Docs| |Version| |Downloads| 5 | 6 | .. raw:: html 7 | 8 | 9 | 10 | Python has all kinds of data validation tools, but every one of them 11 | seems to require defining a schema or form. I wanted to create a simple 12 | validation library where validating a simple value does not require 13 | defining a form or a schema. 14 | 15 | .. code:: shell 16 | 17 | pip install validators 18 | 19 | Then, 20 | 21 | .. code:: python 22 | 23 | >>> import validators 24 | >>> 25 | >>> validators.email('someone@example.com') 26 | True 27 | 28 | Resources 29 | --------- 30 | 31 | .. raw:: html 32 | 33 | 34 | 35 | .. raw:: html 36 | 37 | 38 | 39 | - `Documentation `__ 40 | - `Bugtracker `__ 41 | - `Security `__ 42 | - `Code `__ 43 | 44 | .. raw:: html 45 | 46 | 47 | 48 | -------------- 49 | 50 | **Python 3.9** `reaches EOL in `__ 51 | **October 2025.** 52 | 53 | .. raw:: html 54 | 55 | 56 | 57 | .. raw:: html 58 | 59 | 61 | 62 | .. |PyCQA| image:: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml/badge.svg 63 | :target: https://github.com/python-validators/validators/actions/workflows/pycqa.yaml 64 | .. |SAST| image:: https://github.com/python-validators/validators/actions/workflows/sast.yaml/badge.svg 65 | :target: https://github.com/python-validators/validators/actions/workflows/sast.yaml 66 | .. |Docs| image:: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment/badge.svg 67 | :target: https://github.com/yozachar/pyvalidators/actions/workflows/pages/pages-build-deployment 68 | .. |Version| image:: https://img.shields.io/pypi/v/validators?logo=pypi&logoColor=white&label=version&color=blue 69 | :target: https://pypi.python.org/pypi/validators/ 70 | .. |Downloads| image:: https://img.shields.io/pypi/dm/validators?logo=pypi&logoColor=white&color=blue 71 | :target: https://pypi.python.org/pypi/validators/ 72 | 73 | 74 | .. toctree:: 75 | :hidden: 76 | :maxdepth: 2 77 | :caption: Quick Start: 78 | :glob: 79 | 80 | install_and_use 81 | 82 | .. toctree:: 83 | :hidden: 84 | :maxdepth: 2 85 | :caption: API Reference: 86 | :glob: 87 | 88 | api/* 89 | -------------------------------------------------------------------------------- /docs/install_and_use.md: -------------------------------------------------------------------------------- 1 | # Install and Use 2 | 3 | ## Installation 4 | 5 | Execute the following command: 6 | 7 | ```text 8 | pip install validators 9 | ``` 10 | 11 | > It's preferable to use `pip` within a virtual environment. 12 | 13 | ## Usage 14 | 15 | ```python 16 | import validators 17 | print(validators.email('someone@example.com')) 18 | ``` 19 | 20 | ### To raise validation error 21 | 22 | 1. Either set the environment variable `RAISE_VALIDATION_ERROR` to `True` 23 | 24 | ```console 25 | $ export RAISE_VALIDATION_ERROR=True 26 | $ python -c "from validators import url; print(url('https//bad_url'))" 27 | Traceback (most recent call last): 28 | File "", line 1, in 29 | File "/path/to/lib/validators/utils.py", line 87, in wrapper 30 | raise ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 31 | validators.utils.ValidationError: ValidationError(func=url, args={'value': 'https//bad_url'}) 32 | ``` 33 | 34 | 2. Or pass `r_ve=True` to each caller function: 35 | 36 | ```console 37 | $ python -c "from validators.card import visa; print(visa('bad_visa_number', r_ve=True))" 38 | Traceback (most recent call last): 39 | File "", line 1, in 40 | File "/path/to/lib/validators/utils.py", line 87, in wrapper 41 | raise ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 42 | validators.utils.ValidationError: ValidationError(func=visa, args={'value': 'bad_visa_number'}) 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/install_and_use.rst: -------------------------------------------------------------------------------- 1 | Install and Use 2 | =============== 3 | 4 | Installation 5 | ------------ 6 | 7 | Execute the following command: 8 | 9 | .. code:: text 10 | 11 | pip install validators 12 | 13 | .. 14 | 15 | It's preferable to use ``pip`` within a virtual environment. 16 | 17 | Usage 18 | ----- 19 | 20 | .. code:: python 21 | 22 | import validators 23 | print(validators.email('someone@example.com')) 24 | 25 | To raise validation error 26 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | 1. Either set the environment variable ``RAISE_VALIDATION_ERROR`` to 29 | ``True`` 30 | 31 | .. code:: console 32 | 33 | $ export RAISE_VALIDATION_ERROR=True 34 | $ python -c "from validators import url; print(url('https//bad_url'))" 35 | Traceback (most recent call last): 36 | File "", line 1, in 37 | File "/path/to/lib/validators/utils.py", line 87, in wrapper 38 | raise ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 39 | validators.utils.ValidationError: ValidationError(func=url, args={'value': 'https//bad_url'}) 40 | 41 | 2. Or pass ``r_ve=True`` to each caller function: 42 | 43 | .. code:: console 44 | 45 | $ python -c "from validators.card import visa; print(visa('bad_visa_number', r_ve=True))" 46 | Traceback (most recent call last): 47 | File "", line 1, in 48 | File "/path/to/lib/validators/utils.py", line 87, in wrapper 49 | raise ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 50 | validators.utils.ValidationError: ValidationError(func=visa, args={'value': 'bad_visa_number'}) 51 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: "validators" 2 | site_description: "Automatic documentation from sources, for MkDocs." 3 | site_url: "https://yozachar.github.io/pyvalidators/" 4 | repo_url: "https://github.com/python-validators/validators/" 5 | edit_uri: "edit/master/docs/" 6 | repo_name: "validators/validators" 7 | site_dir: "site" 8 | watch: [README.md, src/validators/] 9 | 10 | theme: 11 | name: material 12 | icon: 13 | logo: material/marker-check 14 | favicon: assets/icons/favicon.svg 15 | font: 16 | text: Inter 17 | code: "Fira Code" 18 | features: 19 | - content.code.copy 20 | palette: 21 | - media: "(prefers-color-scheme: light)" 22 | scheme: default 23 | primary: white 24 | accent: teal 25 | toggle: 26 | icon: material/toggle-switch 27 | name: Switch to dark mode 28 | - media: "(prefers-color-scheme: dark)" 29 | scheme: slate 30 | primary: black 31 | accent: teal 32 | toggle: 33 | icon: material/toggle-switch-off-outline 34 | name: Switch to light mode 35 | 36 | markdown_extensions: 37 | - toc: 38 | permalink: true 39 | - pymdownx.superfences 40 | - pymdownx.highlight: 41 | use_pygments: true 42 | - pymdownx.inlinehilite 43 | 44 | plugins: 45 | - search 46 | - mkdocstrings: 47 | handlers: 48 | python: 49 | options: 50 | show_root_heading: true 51 | import: 52 | - https://docs.python-requests.org/en/master/objects.inv 53 | - git-revision-date-localized 54 | - mike 55 | 56 | extra: 57 | social: 58 | - icon: fontawesome/brands/github 59 | link: https://github.com/python-validators 60 | - icon: fontawesome/brands/python 61 | link: https://pypi.org/project/validators 62 | version: 63 | provider: mike 64 | default: stable 65 | 66 | copyright: Copyright © 2013 - 2025 Konsta Vesterinen 67 | 68 | nav: 69 | - Home: index.md 70 | - Install and Use: install_and_use.md 71 | - API: 72 | - api/between.md 73 | - api/crypto_addresses.md 74 | - api/card.md 75 | - api/country.md 76 | - api/cron.md 77 | - api/domain.md 78 | - api/email.md 79 | - api/encoding.md 80 | - api/finance.md 81 | - api/hashes.md 82 | - api/hostname.md 83 | - api/i18n.md 84 | - api/iban.md 85 | - api/ip_address.md 86 | - api/length.md 87 | - api/mac_address.md 88 | - api/slug.md 89 | - api/url.md 90 | - api/utils.md 91 | - api/uuid.md 92 | -------------------------------------------------------------------------------- /package/export/__init__.py: -------------------------------------------------------------------------------- 1 | """Export.""" 2 | -------------------------------------------------------------------------------- /package/export/__main__.py: -------------------------------------------------------------------------------- 1 | """Generate docs.""" 2 | 3 | # standard 4 | from ast import ImportFrom, parse 5 | from os import getenv 6 | from os.path import getsize 7 | from pathlib import Path 8 | from shutil import copy 9 | from subprocess import Popen # nosec 10 | 11 | 12 | def _write_ref_content(source: Path, module_name: str, func_name: str): 13 | """Write content.""" 14 | with open(source, "at") as ref: 15 | ref.write( 16 | ( 17 | (f"# {module_name}\n\n" if getsize(source) == 0 else "") 18 | + f"::: validators.{module_name}.{func_name}\n" 19 | ) 20 | if f"{source}".endswith(".md") 21 | else ( 22 | ( 23 | f"{module_name}\n{len(module_name) * '-'}\n\n" 24 | + f".. module:: validators.{module_name}\n" 25 | if getsize(source) == 0 26 | else "" 27 | ) 28 | + f".. autofunction:: {func_name}\n" 29 | ) 30 | ) 31 | 32 | 33 | def _parse_package(source: Path): 34 | """Parse validators package.""" 35 | v_ast = parse(source.read_text(), source) 36 | for namespace in (node for node in v_ast.body if isinstance(node, ImportFrom)): 37 | if not namespace.module: 38 | continue 39 | yield (namespace.module, namespace.names) 40 | 41 | 42 | def _gen_md_docs(source: Path, refs_path: Path): 43 | """Generate Markdown docs.""" 44 | # remove existing markdown files 45 | for md_files in (source / "docs/api").glob("*.md"): 46 | md_files.unlink() 47 | # generate md reference documentation 48 | for module_name, aliases in _parse_package(source / "src/validators/__init__.py"): 49 | for alias in aliases: 50 | _write_ref_content(refs_path / f"{module_name}.md", module_name, alias.name) 51 | # build mkdocs as subprocess 52 | mkdocs_build = Popen(("mkdocs", "build")) # nosec 53 | mkdocs_build.communicate() 54 | return mkdocs_build.returncode 55 | 56 | 57 | def _gen_rst_docs(source: Path, refs_path: Path, only_web: bool = False, only_man: bool = False): 58 | """Generate reStructuredText docs.""" 59 | # external 60 | from pypandoc import convert_file # type: ignore 61 | 62 | # remove existing rST files 63 | for rst_files in (source / "docs/api").glob("*.rst"): 64 | rst_files.unlink() 65 | 66 | with open(source / "docs/index.rst", "wt") as idx_f: 67 | idx_f.write( 68 | convert_file(source_file=source / "docs/index.md", format="md", to="rst").replace( 69 | "\r\n", 70 | "\n", # remove carriage return in windows 71 | ) 72 | + "\n\n.. toctree::" 73 | + "\n :hidden:" 74 | + "\n :maxdepth: 2" 75 | + "\n :caption: Quick Start:" 76 | + "\n :glob:\n" 77 | + "\n install_and_use" 78 | + "\n\n.. toctree::" 79 | + "\n :hidden:" 80 | + "\n :maxdepth: 2" 81 | + "\n :caption: API Reference:" 82 | + "\n :glob:\n" 83 | + "\n api/*\n" 84 | ) 85 | 86 | with open(source / "docs/install_and_use.rst", "wt") as iau_f: 87 | iau_f.write( 88 | convert_file(source_file=source / "docs/install_and_use.md", format="md", to="rst") 89 | .replace("\r\n", "\n") # remove carriage return in windows 90 | .replace("’", "'") 91 | ) 92 | 93 | # generate rST reference documentation 94 | for module_name, aliases in _parse_package(source / "src/validators/__init__.py"): 95 | for alias in aliases: 96 | _write_ref_content(refs_path / f"{module_name}.rst", module_name, alias.name) 97 | exit_code = 0 98 | if not only_man: 99 | # build sphinx web pages as subprocess 100 | web_build = Popen(("sphinx-build", "docs", "docs/_build/web"), shell=False) # nosec 101 | web_build.communicate() 102 | exit_code = web_build.returncode 103 | print("Run `python -m http.server -d docs/_build/web` to preview.") 104 | if not only_web: 105 | # build sphinx man pages as subprocess 106 | man_build = Popen( # nosec 107 | ("sphinx-build", "-b", "man", "docs", "docs/_build/man"), shell=False 108 | ) 109 | man_build.communicate() 110 | copy(source / "docs/_build/man/validators.1", source / "docs/validators.1") 111 | print(f"Man page copied to: {source / 'docs/validators.1'}") 112 | exit_code = man_build.returncode if exit_code == 0 else exit_code 113 | return exit_code 114 | 115 | 116 | def _generate_documentation( 117 | source: Path, 118 | only_md: bool = False, 119 | only_rst_web: bool = False, 120 | only_rst_man: bool = False, 121 | ): 122 | """Generate documentation.""" 123 | if only_md is only_rst_web is only_rst_man is True: 124 | return 125 | if only_md is only_rst_web is only_rst_man is False: 126 | return 127 | # copy readme as docs index file 128 | copy(source / "README.md", source / "docs/index.md") 129 | # clean destination 130 | refs_path = source / "docs/api" 131 | # if refs_path.is_dir(): 132 | # rmtree(refs_path) 133 | refs_path.mkdir(exist_ok=True) 134 | exit_code = 0 if (only_rst_web or only_rst_man) else _gen_md_docs(source, refs_path) 135 | if not only_md: 136 | exit_code = ( 137 | _gen_rst_docs(source, refs_path, only_rst_web, only_rst_man) 138 | if exit_code == 0 139 | else exit_code 140 | ) 141 | return exit_code 142 | 143 | 144 | def package(source: Path): 145 | """Package the source code.""" 146 | _generate_documentation(source, only_rst_man=True) 147 | if getenv("CI", "false") == "true": 148 | process = Popen(("./.venv/bin/python", "-m", "build"), shell=False) # nosec 149 | else: 150 | process = Popen(("pdm", "build"), shell=False) # nosec 151 | process.communicate() 152 | return process.returncode 153 | 154 | 155 | if __name__ == "__main__": 156 | project_root = Path(__file__).parent.parent.parent 157 | exit_code = 0 158 | 159 | # standard 160 | from sys import argv 161 | 162 | if len(argv) != 2: 163 | print("Expected one of these augments: `pkg` `doc` `man` or `web`") 164 | quit(1) 165 | 166 | if argv[1] == "pkg": 167 | exit_code = package(project_root) 168 | elif argv[1] == "doc": 169 | exit_code = _generate_documentation(project_root, only_md=True) 170 | elif argv[1] == "man": 171 | exit_code = _generate_documentation(project_root, only_rst_man=True) 172 | elif argv[1] == "web": 173 | exit_code = _generate_documentation(project_root, only_rst_web=True) 174 | quit(exit_code) 175 | 176 | # TODO: Address all '# nosec' 177 | -------------------------------------------------------------------------------- /package/requirements.testing.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | colorama==0.4.6 \ 5 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 6 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 7 | eth-hash[pycryptodome]==0.7.0 \ 8 | --hash=sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f \ 9 | --hash=sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a 10 | exceptiongroup==1.2.1; python_version < "3.11" \ 11 | --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \ 12 | --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16 13 | iniconfig==2.0.0 \ 14 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ 15 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 16 | packaging==24.1 \ 17 | --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ 18 | --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 19 | pluggy==1.5.0 \ 20 | --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ 21 | --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 22 | pycryptodome==3.20.0 \ 23 | --hash=sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7 \ 24 | --hash=sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4 \ 25 | --hash=sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5 \ 26 | --hash=sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab \ 27 | --hash=sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a \ 28 | --hash=sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25 \ 29 | --hash=sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea \ 30 | --hash=sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a \ 31 | --hash=sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c \ 32 | --hash=sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72 \ 33 | --hash=sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9 \ 34 | --hash=sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6 \ 35 | --hash=sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044 \ 36 | --hash=sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04 \ 37 | --hash=sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c \ 38 | --hash=sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e \ 39 | --hash=sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b \ 40 | --hash=sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e \ 41 | --hash=sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2 \ 42 | --hash=sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3 \ 43 | --hash=sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128 44 | pytest==8.3.2 \ 45 | --hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \ 46 | --hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce 47 | tomli==2.0.1; python_version < "3.11" \ 48 | --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ 49 | --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f 50 | -------------------------------------------------------------------------------- /package/requirements.tooling.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | colorama==0.4.6 \ 5 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 6 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 7 | eth-hash[pycryptodome]==0.7.0 \ 8 | --hash=sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f \ 9 | --hash=sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a 10 | exceptiongroup==1.2.1; python_version < "3.11" \ 11 | --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \ 12 | --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16 13 | iniconfig==2.0.0 \ 14 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ 15 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 16 | nodeenv==1.9.1 \ 17 | --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ 18 | --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 19 | packaging==24.1 \ 20 | --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ 21 | --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 22 | pluggy==1.5.0 \ 23 | --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ 24 | --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 25 | pycryptodome==3.20.0 \ 26 | --hash=sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7 \ 27 | --hash=sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4 \ 28 | --hash=sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5 \ 29 | --hash=sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab \ 30 | --hash=sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a \ 31 | --hash=sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25 \ 32 | --hash=sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea \ 33 | --hash=sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a \ 34 | --hash=sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c \ 35 | --hash=sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72 \ 36 | --hash=sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9 \ 37 | --hash=sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6 \ 38 | --hash=sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044 \ 39 | --hash=sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04 \ 40 | --hash=sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c \ 41 | --hash=sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e \ 42 | --hash=sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b \ 43 | --hash=sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e \ 44 | --hash=sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2 \ 45 | --hash=sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3 \ 46 | --hash=sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128 47 | pypandoc-binary==1.13 \ 48 | --hash=sha256:11a2497320eb3dccb74de3c67b6df3e5d3f66cdc2a36a67e9a871708f7e48412 \ 49 | --hash=sha256:21ef0345726d36fc45a50211320614daf2caede684b0d0963ce8738292809746 \ 50 | --hash=sha256:2915f52e4632bd2d0a8fcd2f7e7dfc2ea19b4e1a280fcbc2ddcd142713c4ff12 \ 51 | --hash=sha256:3881aa7c84faec2007c0ae4466d3a1cfc93171206b8540f2defa8ea971bf6fd6 \ 52 | --hash=sha256:67c0c7af811bcf3cd4f3221be756a4975ec35b2d7df89d8de4313a8caa2cd54f \ 53 | --hash=sha256:9455fdd9521cbf4b56d79a56b806afa94c8c22f3c8ef878536e58d941a70f6d6 \ 54 | --hash=sha256:946666388eb79b307d7f497b3b33045ef807750f8e5ef3440e0ba3bbab698044 55 | pyright==1.1.378 \ 56 | --hash=sha256:78a043be2876d12d0af101d667e92c7734f3ebb9db71dccc2c220e7e7eb89ca2 \ 57 | --hash=sha256:8853776138b01bc284da07ac481235be7cc89d3176b073d2dba73636cb95be79 58 | pytest==8.3.2 \ 59 | --hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \ 60 | --hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce 61 | ruff==0.6.3 \ 62 | --hash=sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82 \ 63 | --hash=sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983 \ 64 | --hash=sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1 \ 65 | --hash=sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc \ 66 | --hash=sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500 \ 67 | --hash=sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1 \ 68 | --hash=sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a \ 69 | --hash=sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f \ 70 | --hash=sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470 \ 71 | --hash=sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb \ 72 | --hash=sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521 \ 73 | --hash=sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384 \ 74 | --hash=sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3 \ 75 | --hash=sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1 \ 76 | --hash=sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672 \ 77 | --hash=sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5 \ 78 | --hash=sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351 \ 79 | --hash=sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8 80 | tomli==2.0.1; python_version < "3.11" \ 81 | --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ 82 | --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f 83 | -------------------------------------------------------------------------------- /package/roll.ps1: -------------------------------------------------------------------------------- 1 | #!/bin/pwsh 2 | 3 | $ErrorActionPreference = "Stop" 4 | 5 | # Check if CI environment variable is set to "false" 6 | if ($null -eq $env:CI || "false" -eq $env:CI) { 7 | # testing 8 | pdm export --group testing,crypto-eth-addresses -f requirements -o package/requirements.testing.txt 9 | # tooling 10 | pdm export --group tooling,crypto-eth-addresses -f requirements -o package/requirements.tooling.txt 11 | # mkdocs 12 | # pdm export --group docs-online -f requirements -o package/requirements.mkdocs.txt 13 | # sphinx 14 | pdm export --group docs-offline,crypto-eth-addresses -f requirements -o package/requirements.sphinx.txt 15 | 16 | # create environment variable 17 | $env:CI = "true"; 18 | } 19 | 20 | # Cleanup directories 21 | $venv_dir = "./.venv.dev" 22 | $directories = @($venv_dir, "./build", "./dist") 23 | foreach ($dir in $directories) { 24 | if (Test-Path $dir -PathType Container) { 25 | Remove-Item $dir -Recurse -Force 26 | } 27 | } 28 | 29 | # Create venv 30 | python -m venv $venv_dir 31 | 32 | 33 | $bin_path = "Scripts" 34 | if ($IsLinux || $IsMacOS) { 35 | $bin_path = "bin" 36 | } 37 | 38 | # Upgrade pip 39 | & $venv_dir\$bin_path\python -m pip install --upgrade pip 40 | 41 | # Install the current package 42 | & $venv_dir\$bin_path\pip install . 43 | 44 | # Install sphinx requirements 45 | & $venv_dir\$bin_path\pip install -r package/requirements.sphinx.txt 46 | 47 | # Install build tool 48 | & $venv_dir\$bin_path\pip install build 49 | 50 | # Activate virtual environment 51 | . $venv_dir\$bin_path\Activate.ps1 52 | 53 | # Run export script 54 | python package/export pkg 55 | 56 | # Deactivate virtual environment 57 | deactivate 58 | 59 | # delete environment variable 60 | $env:CI = ""; 61 | -------------------------------------------------------------------------------- /package/roll.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if CI environment variable is set to "false" 6 | # or set to empty string or not set at all. 7 | # Using the wrong way see: https://stackoverflow.com/a/13864829 8 | if [ -z "$CI" ] || [ "$CI" = "false" ]; then 9 | # testing 10 | pdm export --group testing,crypto-eth-addresses -f requirements -o package/requirements.testing.txt 11 | # tooling 12 | pdm export --group tooling,crypto-eth-addresses -f requirements -o package/requirements.tooling.txt 13 | # mkdocs 14 | # pdm export --group docs-online -f requirements -o package/requirements.mkdocs.txt 15 | # sphinx 16 | pdm export --group docs-offline,crypto-eth-addresses -f requirements -o package/requirements.sphinx.txt 17 | export CI=true 18 | fi 19 | 20 | # Cleanup directories 21 | venv_dir="./.venv.dev" 22 | directories=("$venv_dir" "./build" "./dist") 23 | for dir in "${directories[@]}"; do 24 | if [ -d "$dir" ]; then 25 | rm -rf "$dir" 26 | fi 27 | done 28 | 29 | # Create venv 30 | python -m venv $venv_dir 31 | 32 | # Upgrade pip 33 | $venv_dir/bin/python -m pip install --upgrade pip 34 | 35 | # Install the current package 36 | $venv_dir/bin/pip install . 37 | 38 | # Install sphinx requirements 39 | $venv_dir/bin/pip install -r package/requirements.sphinx.txt 40 | 41 | # Install build tool 42 | $venv_dir/bin/pip install build 43 | 44 | # Activate virtual environment 45 | . $venv_dir/bin/activate 46 | 47 | # Run export script 48 | python package/export pkg 49 | 50 | # Deactivate virtual environment 51 | deactivate 52 | 53 | # delete environment variable 54 | unset CI 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | #################### 2 | # Build System # 3 | #################### 4 | 5 | [build-system] 6 | requires = ["setuptools"] 7 | build-backend = "setuptools.build_meta" 8 | 9 | #################### 10 | # Metadata # 11 | #################### 12 | 13 | [project] 14 | name = "validators" 15 | description = "Python Data Validation for Humans™" 16 | authors = [{ name = "Konsta Vesterinen", email = "konsta@fastmonkeys.com" }] 17 | license = { text = "MIT" } 18 | readme = "README.md" 19 | keywords = ["validation", "validator", "python-validator"] 20 | classifiers = [ 21 | "Development Status :: 4 - Beta", 22 | "Environment :: Web Environment", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | ] 36 | requires-python = ">=3.9" 37 | dynamic = ["version"] 38 | dependencies = [] 39 | 40 | [project.urls] 41 | Homepage = "https://python-validators.github.io/validators" 42 | Documentation = "https://yozachar.github.io/pyvalidators" 43 | Repository = "https://github.com/python-validators/validators" 44 | Changelog = "https://github.com/python-validators/validators/blob/master/CHANGES.md" 45 | 46 | ########################### 47 | # Optional Dependencies # 48 | ########################### 49 | 50 | [project.optional-dependencies] 51 | crypto-eth-addresses = ["eth-hash[pycryptodome]>=0.7.0"] 52 | 53 | ############################## 54 | # Development Dependencies # 55 | ############################## 56 | 57 | [tool.pdm.dev-dependencies] 58 | docs-offline = [ 59 | "myst-parser>=3.0.1", 60 | "pypandoc-binary>=1.13", 61 | "sphinx>=7.1.2", 62 | "furo>=2024.8.6", 63 | ] 64 | docs-online = [ 65 | "mkdocs>=1.6.1", 66 | "mkdocs-git-revision-date-localized-plugin>=1.2.7", 67 | "mkdocs-material>=9.5.34", 68 | "mkdocstrings[python]>=0.26.0", 69 | "mike>=2.1.3", 70 | ] 71 | package = ["build>=1.2.1"] 72 | runner = ["tox>=4.18.0"] 73 | sast = ["bandit[toml]>=1.7.9"] 74 | testing = ["pytest>=8.3.2"] 75 | tooling = [ 76 | "ruff>=0.6.3", 77 | "pyright>=1.1.378", 78 | "pytest>=8.3.2", 79 | "pypandoc-binary>=1.13", # helps with type checking 80 | ] 81 | 82 | #################### 83 | # Configurations # 84 | #################### 85 | 86 | [tool.setuptools.packages.find] 87 | where = ["src"] 88 | include = ["validators*"] 89 | namespaces = false 90 | 91 | [tool.setuptools.package-data] 92 | validators = ["py.typed", "_tld.txt"] 93 | 94 | [tool.setuptools.dynamic] 95 | version = { attr = "validators.__version__" } 96 | 97 | [tool.bandit] 98 | exclude_dirs = [ 99 | ".github", 100 | ".pytest_cache", 101 | ".tox", 102 | ".venv", 103 | ".venv.dev", 104 | ".vscode", 105 | "site", 106 | "tests", 107 | ] 108 | 109 | [tool.pyright] 110 | extraPaths = ["src"] 111 | exclude = [ 112 | "**/__pycache__/", 113 | ".pytest_cache/", 114 | ".tox/", 115 | ".venv/", 116 | ".venv.dev/", 117 | "site/", 118 | ] 119 | pythonVersion = "3.9" 120 | pythonPlatform = "All" 121 | typeCheckingMode = "strict" 122 | 123 | [tool.pytest.ini_options] 124 | minversion = "6.0" 125 | pythonpath = ["src"] 126 | testpaths = "tests" 127 | addopts = ["--doctest-modules"] 128 | 129 | 130 | [tool.ruff] 131 | lint.select = [ 132 | # Pyflakes 133 | "F", 134 | # pycodestyle 135 | "W", 136 | "E", 137 | # mccabe 138 | # C90 139 | # isort 140 | "I", 141 | # pep8-naming 142 | "N", 143 | # pydocstyle 144 | "D", 145 | ] 146 | line-length = 100 147 | target-version = "py39" 148 | extend-exclude = ["**/__pycache__", ".pytest_cache", "site"] 149 | 150 | [tool.ruff.lint.isort] 151 | # case-sensitive = true 152 | combine-as-imports = true 153 | force-sort-within-sections = true 154 | force-wrap-aliases = true 155 | known-local-folder = ["src"] 156 | relative-imports-order = "closest-to-furthest" 157 | 158 | [tool.ruff.lint.pydocstyle] 159 | convention = "google" 160 | 161 | [tool.tox] 162 | legacy_tox_ini = """ 163 | [tox] 164 | requires = 165 | tox>=4 166 | env_list = lint, type, format, sast, py{39,310,311,312,313} 167 | 168 | [testenv:lint] 169 | description = ruff linter 170 | deps = 171 | ruff 172 | commands = ruff check . 173 | 174 | [testenv:type] 175 | description = pyright type checker 176 | deps = 177 | pyright 178 | pypandoc-binary 179 | pytest 180 | .[crypto-eth-addresses] 181 | commands = pyright . 182 | 183 | [testenv:format] 184 | description = code formatter 185 | deps = 186 | ruff 187 | commands = ruff format . 188 | 189 | [testenv:sast] 190 | deps = 191 | bandit[toml] 192 | commands = bandit -c pyproject.toml -r . 193 | 194 | [testenv] 195 | description = unit tests 196 | deps = 197 | pytest 198 | .[crypto-eth-addresses] 199 | commands = pytest . 200 | """ 201 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """Validators.""" 2 | -------------------------------------------------------------------------------- /src/validators/__init__.py: -------------------------------------------------------------------------------- 1 | """Validate Anything!""" 2 | 3 | # local 4 | from .between import between 5 | from .card import amex, card_number, diners, discover, jcb, mastercard, mir, unionpay, visa 6 | from .country import calling_code, country_code, currency 7 | from .cron import cron 8 | from .crypto_addresses import bsc_address, btc_address, eth_address, trx_address 9 | from .domain import domain 10 | from .email import email 11 | from .encoding import base16, base32, base58, base64 12 | from .finance import cusip, isin, sedol 13 | from .hashes import md5, sha1, sha224, sha256, sha384, sha512 14 | from .hostname import hostname 15 | from .i18n import ( 16 | es_cif, 17 | es_doi, 18 | es_nie, 19 | es_nif, 20 | fi_business_id, 21 | fi_ssn, 22 | fr_department, 23 | fr_ssn, 24 | ind_aadhar, 25 | ind_pan, 26 | ru_inn, 27 | ) 28 | from .iban import iban 29 | from .ip_address import ipv4, ipv6 30 | from .length import length 31 | from .mac_address import mac_address 32 | from .slug import slug 33 | from .url import url 34 | from .utils import ValidationError, validator 35 | from .uuid import uuid 36 | 37 | __all__ = ( 38 | # ... 39 | "between", 40 | # crypto_addresses 41 | "bsc_address", 42 | "btc_address", 43 | "eth_address", 44 | "trx_address", 45 | # cards 46 | "amex", 47 | "card_number", 48 | "diners", 49 | "discover", 50 | "jcb", 51 | "mastercard", 52 | "unionpay", 53 | "visa", 54 | "mir", 55 | # country 56 | "calling_code", 57 | "country_code", 58 | "currency", 59 | # ... 60 | "cron", 61 | # ... 62 | "domain", 63 | # ... 64 | "email", 65 | # encodings 66 | "base16", 67 | "base32", 68 | "base58", 69 | "base64", 70 | # finance 71 | "cusip", 72 | "isin", 73 | "sedol", 74 | # hashes 75 | "md5", 76 | "sha1", 77 | "sha224", 78 | "sha256", 79 | "sha384", 80 | "sha512", 81 | # ... 82 | "hostname", 83 | # i18n 84 | "es_cif", 85 | "es_doi", 86 | "es_nie", 87 | "es_nif", 88 | "fi_business_id", 89 | "fi_ssn", 90 | "fr_department", 91 | "fr_ssn", 92 | "ind_aadhar", 93 | "ind_pan", 94 | "ru_inn", 95 | # ... 96 | "iban", 97 | # ip_addresses 98 | "ipv4", 99 | "ipv6", 100 | # ... 101 | "length", 102 | # ... 103 | "mac_address", 104 | # ... 105 | "slug", 106 | # ... 107 | "url", 108 | # ... 109 | "uuid", 110 | # utils 111 | "ValidationError", 112 | "validator", 113 | ) 114 | 115 | __version__ = "0.35.0" 116 | -------------------------------------------------------------------------------- /src/validators/_extremes.py: -------------------------------------------------------------------------------- 1 | """Extremes.""" 2 | 3 | # standard 4 | from functools import total_ordering 5 | from typing import Any 6 | 7 | 8 | @total_ordering 9 | class AbsMax: 10 | """An object that is greater than any other object (except itself). 11 | 12 | Inspired by https://pypi.python.org/pypi/Extremes. 13 | 14 | Examples: 15 | >>> from sys import maxsize 16 | >>> AbsMax() > AbsMin() 17 | True 18 | >>> AbsMax() > maxsize 19 | True 20 | >>> AbsMax() > 99999999999999999 21 | True 22 | """ 23 | 24 | def __ge__(self, other: Any): 25 | """GreaterThanOrEqual.""" 26 | return other is not AbsMax 27 | 28 | 29 | @total_ordering 30 | class AbsMin: 31 | """An object that is less than any other object (except itself). 32 | 33 | Inspired by https://pypi.python.org/pypi/Extremes. 34 | 35 | Examples: 36 | >>> from sys import maxsize 37 | >>> AbsMin() < -maxsize 38 | True 39 | >>> AbsMin() < None 40 | True 41 | >>> AbsMin() < '' 42 | True 43 | """ 44 | 45 | def __le__(self, other: Any): 46 | """LessThanOrEqual.""" 47 | return other is not AbsMin 48 | -------------------------------------------------------------------------------- /src/validators/between.py: -------------------------------------------------------------------------------- 1 | """Between.""" 2 | 3 | # standard 4 | from datetime import datetime 5 | from typing import TypeVar, Union 6 | 7 | # local 8 | from ._extremes import AbsMax, AbsMin 9 | from .utils import validator 10 | 11 | PossibleValueTypes = TypeVar("PossibleValueTypes", int, float, str, datetime, None) 12 | 13 | 14 | @validator 15 | def between( 16 | value: PossibleValueTypes, 17 | /, 18 | *, 19 | min_val: Union[PossibleValueTypes, AbsMin, None] = None, 20 | max_val: Union[PossibleValueTypes, AbsMax, None] = None, 21 | ): 22 | """Validate that a number is between minimum and/or maximum value. 23 | 24 | This will work with any comparable type, such as floats, decimals and dates 25 | not just integers. This validator is originally based on [WTForms-NumberRange-Validator][1]. 26 | 27 | [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py#L166-L220 28 | 29 | Examples: 30 | >>> from datetime import datetime 31 | >>> between(5, min_val=2) 32 | True 33 | >>> between(13.2, min_val=13, max_val=14) 34 | True 35 | >>> between(500, max_val=400) 36 | ValidationError(func=between, args={'value': 500, 'max_val': 400}) 37 | >>> between( 38 | ... datetime(2000, 11, 11), 39 | ... min_val=datetime(1999, 11, 11) 40 | ... ) 41 | True 42 | 43 | Args: 44 | value: 45 | Value which is to be compared. 46 | min_val: 47 | The minimum required value of the number. 48 | If not provided, minimum value will not be checked. 49 | max_val: 50 | The maximum value of the number. 51 | If not provided, maximum value will not be checked. 52 | 53 | Returns: 54 | (Literal[True]): If `value` is in between the given conditions. 55 | (ValidationError): If `value` is not in between the given conditions. 56 | 57 | Raises: 58 | (ValueError): If `min_val` is greater than `max_val`. 59 | (TypeError): If there's a type mismatch during comparison. 60 | 61 | Note: 62 | - `PossibleValueTypes` = `TypeVar("PossibleValueTypes", int, float, str, datetime)` 63 | - If neither `min_val` nor `max_val` is provided, result will always be `True`. 64 | """ 65 | if value is None: 66 | return False 67 | 68 | if max_val is None: 69 | max_val = AbsMax() 70 | if min_val is None: 71 | min_val = AbsMin() 72 | 73 | try: 74 | if min_val > max_val: 75 | raise ValueError("`min_val` cannot be greater than `max_val`") 76 | except TypeError as err: 77 | raise TypeError("Comparison type mismatch") from err 78 | 79 | return min_val <= value <= max_val 80 | -------------------------------------------------------------------------------- /src/validators/card.py: -------------------------------------------------------------------------------- 1 | """Card.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | @validator 11 | def card_number(value: str, /): 12 | """Return whether or not given value is a valid generic card number. 13 | 14 | This validator is based on [Luhn's algorithm][1]. 15 | 16 | [1]: https://github.com/mmcloughlin/luhn 17 | 18 | Examples: 19 | >>> card_number('4242424242424242') 20 | True 21 | >>> card_number('4242424242424241') 22 | ValidationError(func=card_number, args={'value': '4242424242424241'}) 23 | 24 | Args: 25 | value: 26 | Generic card number string to validate 27 | 28 | Returns: 29 | (Literal[True]): If `value` is a valid generic card number. 30 | (ValidationError): If `value` is an invalid generic card number. 31 | """ 32 | if not value: 33 | return False 34 | try: 35 | digits = list(map(int, value)) 36 | odd_sum = sum(digits[-1::-2]) 37 | even_sum = sum(sum(divmod(2 * d, 10)) for d in digits[-2::-2]) 38 | return (odd_sum + even_sum) % 10 == 0 39 | except ValueError: 40 | return False 41 | 42 | 43 | @validator 44 | def visa(value: str, /): 45 | """Return whether or not given value is a valid Visa card number. 46 | 47 | Examples: 48 | >>> visa('4242424242424242') 49 | True 50 | >>> visa('2223003122003222') 51 | ValidationError(func=visa, args={'value': '2223003122003222'}) 52 | 53 | Args: 54 | value: 55 | Visa card number string to validate 56 | 57 | Returns: 58 | (Literal[True]): If `value` is a valid Visa card number. 59 | (ValidationError): If `value` is an invalid Visa card number. 60 | """ 61 | pattern = re.compile(r"^4") 62 | return card_number(value) and len(value) == 16 and pattern.match(value) 63 | 64 | 65 | @validator 66 | def mastercard(value: str, /): 67 | """Return whether or not given value is a valid Mastercard card number. 68 | 69 | Examples: 70 | >>> mastercard('5555555555554444') 71 | True 72 | >>> mastercard('4242424242424242') 73 | ValidationError(func=mastercard, args={'value': '4242424242424242'}) 74 | 75 | Args: 76 | value: 77 | Mastercard card number string to validate 78 | 79 | Returns: 80 | (Literal[True]): If `value` is a valid Mastercard card number. 81 | (ValidationError): If `value` is an invalid Mastercard card number. 82 | """ 83 | pattern = re.compile(r"^(51|52|53|54|55|22|23|24|25|26|27)") 84 | return card_number(value) and len(value) == 16 and pattern.match(value) 85 | 86 | 87 | @validator 88 | def amex(value: str, /): 89 | """Return whether or not given value is a valid American Express card number. 90 | 91 | Examples: 92 | >>> amex('378282246310005') 93 | True 94 | >>> amex('4242424242424242') 95 | ValidationError(func=amex, args={'value': '4242424242424242'}) 96 | 97 | Args: 98 | value: 99 | American Express card number string to validate 100 | 101 | Returns: 102 | (Literal[True]): If `value` is a valid American Express card number. 103 | (ValidationError): If `value` is an invalid American Express card number. 104 | """ 105 | pattern = re.compile(r"^(34|37)") 106 | return card_number(value) and len(value) == 15 and pattern.match(value) 107 | 108 | 109 | @validator 110 | def unionpay(value: str, /): 111 | """Return whether or not given value is a valid UnionPay card number. 112 | 113 | Examples: 114 | >>> unionpay('6200000000000005') 115 | True 116 | >>> unionpay('4242424242424242') 117 | ValidationError(func=unionpay, args={'value': '4242424242424242'}) 118 | 119 | Args: 120 | value: 121 | UnionPay card number string to validate 122 | 123 | Returns: 124 | (Literal[True]): If `value` is a valid UnionPay card number. 125 | (ValidationError): If `value` is an invalid UnionPay card number. 126 | """ 127 | pattern = re.compile(r"^62") 128 | return card_number(value) and len(value) == 16 and pattern.match(value) 129 | 130 | 131 | @validator 132 | def diners(value: str, /): 133 | """Return whether or not given value is a valid Diners Club card number. 134 | 135 | Examples: 136 | >>> diners('3056930009020004') 137 | True 138 | >>> diners('4242424242424242') 139 | ValidationError(func=diners, args={'value': '4242424242424242'}) 140 | 141 | Args: 142 | value: 143 | Diners Club card number string to validate 144 | 145 | Returns: 146 | (Literal[True]): If `value` is a valid Diners Club card number. 147 | (ValidationError): If `value` is an invalid Diners Club card number. 148 | """ 149 | pattern = re.compile(r"^(30|36|38|39)") 150 | return card_number(value) and len(value) in {14, 16} and pattern.match(value) 151 | 152 | 153 | @validator 154 | def jcb(value: str, /): 155 | """Return whether or not given value is a valid JCB card number. 156 | 157 | Examples: 158 | >>> jcb('3566002020360505') 159 | True 160 | >>> jcb('4242424242424242') 161 | ValidationError(func=jcb, args={'value': '4242424242424242'}) 162 | 163 | Args: 164 | value: 165 | JCB card number string to validate 166 | 167 | Returns: 168 | (Literal[True]): If `value` is a valid JCB card number. 169 | (ValidationError): If `value` is an invalid JCB card number. 170 | """ 171 | pattern = re.compile(r"^35") 172 | return card_number(value) and len(value) == 16 and pattern.match(value) 173 | 174 | 175 | @validator 176 | def discover(value: str, /): 177 | """Return whether or not given value is a valid Discover card number. 178 | 179 | Examples: 180 | >>> discover('6011111111111117') 181 | True 182 | >>> discover('4242424242424242') 183 | ValidationError(func=discover, args={'value': '4242424242424242'}) 184 | 185 | Args: 186 | value: 187 | Discover card number string to validate 188 | 189 | Returns: 190 | (Literal[True]): If `value` is a valid Discover card number. 191 | (ValidationError): If `value` is an invalid Discover card number. 192 | """ 193 | pattern = re.compile(r"^(60|64|65)") 194 | return card_number(value) and len(value) == 16 and pattern.match(value) 195 | 196 | 197 | @validator 198 | def mir(value: str, /): 199 | """Return whether or not given value is a valid Mir card number. 200 | 201 | Examples: 202 | >>> mir('2200123456789019') 203 | True 204 | >>> mir('4242424242424242') 205 | ValidationError(func=mir, args={'value': '4242424242424242'}) 206 | 207 | Args: 208 | value: 209 | Mir card number string to validate. 210 | 211 | Returns: 212 | (Literal[True]): If `value` is a valid Mir card number. 213 | (ValidationError): If `value` is an invalid Mir card number. 214 | """ 215 | pattern = re.compile(r"^(220[0-4])") 216 | return card_number(value) and len(value) == 16 and pattern.match(value) 217 | -------------------------------------------------------------------------------- /src/validators/cron.py: -------------------------------------------------------------------------------- 1 | """Cron.""" 2 | 3 | # local 4 | from .utils import validator 5 | 6 | 7 | def _validate_cron_component(component: str, min_val: int, max_val: int): 8 | if component == "*": 9 | return True 10 | 11 | if component.isdecimal(): 12 | return min_val <= int(component) <= max_val 13 | 14 | if "/" in component: 15 | parts = component.split("/") 16 | if len(parts) != 2 or not parts[1].isdecimal() or int(parts[1]) < 1: 17 | return False 18 | if parts[0] == "*": 19 | return True 20 | return parts[0].isdecimal() and min_val <= int(parts[0]) <= max_val 21 | 22 | if "-" in component: 23 | parts = component.split("-") 24 | if len(parts) != 2 or not parts[0].isdecimal() or not parts[1].isdecimal(): 25 | return False 26 | start, end = int(parts[0]), int(parts[1]) 27 | return min_val <= start <= max_val and min_val <= end <= max_val and start <= end 28 | 29 | if "," in component: 30 | for item in component.split(","): 31 | if not _validate_cron_component(item, min_val, max_val): 32 | return False 33 | return True 34 | # return all( 35 | # _validate_cron_component(item, min_val, max_val) for item in component.split(",") 36 | # ) # throws type error. why? 37 | 38 | return False 39 | 40 | 41 | @validator 42 | def cron(value: str, /): 43 | """Return whether or not given value is a valid cron string. 44 | 45 | Examples: 46 | >>> cron('*/5 * * * *') 47 | True 48 | >>> cron('30-20 * * * *') 49 | ValidationError(func=cron, args={'value': '30-20 * * * *'}) 50 | 51 | Args: 52 | value: 53 | Cron string to validate. 54 | 55 | Returns: 56 | (Literal[True]): If `value` is a valid cron string. 57 | (ValidationError): If `value` is an invalid cron string. 58 | """ 59 | if not value: 60 | return False 61 | 62 | try: 63 | minutes, hours, days, months, weekdays = value.strip().split() 64 | except ValueError as err: 65 | raise ValueError("Badly formatted cron string") from err 66 | 67 | if not _validate_cron_component(minutes, 0, 59): 68 | return False 69 | if not _validate_cron_component(hours, 0, 23): 70 | return False 71 | if not _validate_cron_component(days, 1, 31): 72 | return False 73 | if not _validate_cron_component(months, 1, 12): 74 | return False 75 | if not _validate_cron_component(weekdays, 0, 6): 76 | return False 77 | 78 | return True 79 | -------------------------------------------------------------------------------- /src/validators/crypto_addresses/__init__.py: -------------------------------------------------------------------------------- 1 | """Crypto addresses.""" 2 | 3 | # local 4 | from .bsc_address import bsc_address 5 | from .btc_address import btc_address 6 | from .eth_address import eth_address 7 | from .trx_address import trx_address 8 | 9 | __all__ = ("bsc_address", "btc_address", "eth_address", "trx_address") 10 | -------------------------------------------------------------------------------- /src/validators/crypto_addresses/bsc_address.py: -------------------------------------------------------------------------------- 1 | """BSC Address.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from validators.utils import validator 8 | 9 | 10 | @validator 11 | def bsc_address(value: str, /): 12 | """Return whether or not given value is a valid binance smart chain address. 13 | 14 | Full validation is implemented for BSC addresses. 15 | 16 | Examples: 17 | >>> bsc_address('0x4e5acf9684652BEa56F2f01b7101a225Ee33d23f') 18 | True 19 | >>> bsc_address('0x4g5acf9684652BEa56F2f01b7101a225Eh33d23z') 20 | ValidationError(func=bsc_address, args={'value': '0x4g5acf9684652BEa56F2f01b7101a225Eh33d23z'}) 21 | 22 | Args: 23 | value: 24 | BSC address string to validate. 25 | 26 | Returns: 27 | (Literal[True]): If `value` is a valid bsc address. 28 | (ValidationError): If `value` is an invalid bsc address. 29 | """ # noqa: E501 30 | if not value: 31 | return False 32 | 33 | if not re.fullmatch(r"0x[a-fA-F0-9]{40}", value): 34 | return False 35 | 36 | return True 37 | -------------------------------------------------------------------------------- /src/validators/crypto_addresses/btc_address.py: -------------------------------------------------------------------------------- 1 | """BTC Address.""" 2 | 3 | # standard 4 | from hashlib import sha256 5 | import re 6 | 7 | # local 8 | from validators.utils import validator 9 | 10 | 11 | def _decode_base58(addr: str): 12 | """Decode base58.""" 13 | alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 14 | return sum((58**enm) * alphabet.index(idx) for enm, idx in enumerate(addr[::-1])) 15 | 16 | 17 | def _validate_old_btc_address(addr: str): 18 | """Validate P2PKH and P2SH type address.""" 19 | if len(addr) not in range(25, 35): 20 | return False 21 | decoded_bytes = _decode_base58(addr).to_bytes(25, "big") 22 | header, checksum = decoded_bytes[:-4], decoded_bytes[-4:] 23 | return checksum == sha256(sha256(header).digest()).digest()[:4] 24 | 25 | 26 | @validator 27 | def btc_address(value: str, /): 28 | """Return whether or not given value is a valid bitcoin address. 29 | 30 | Full validation is implemented for P2PKH and P2SH addresses. 31 | For segwit addresses a regexp is used to provide a reasonable 32 | estimate on whether the address is valid. 33 | 34 | Examples: 35 | >>> btc_address('3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69') 36 | True 37 | >>> btc_address('1BvBMsEYstWetqTFn5Au4m4GFg7xJaNVN2') 38 | ValidationError(func=btc_address, args={'value': '1BvBMsEYstWetqTFn5Au4m4GFg7xJaNVN2'}) 39 | 40 | Args: 41 | value: 42 | Bitcoin address string to validate. 43 | 44 | Returns: 45 | (Literal[True]): If `value` is a valid bitcoin address. 46 | (ValidationError): If `value` is an invalid bitcoin address. 47 | """ 48 | if not value: 49 | return False 50 | 51 | return ( 52 | # segwit pattern 53 | re.compile(r"^(bc|tc)[0-3][02-9ac-hj-np-z]{14,74}$").match(value) 54 | if value[:2] in ("bc", "tb") 55 | else _validate_old_btc_address(value) 56 | ) 57 | -------------------------------------------------------------------------------- /src/validators/crypto_addresses/eth_address.py: -------------------------------------------------------------------------------- 1 | """ETH Address.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from validators.utils import validator 8 | 9 | _keccak_flag = True 10 | try: 11 | # external 12 | from eth_hash.auto import keccak 13 | except ImportError: 14 | _keccak_flag = False 15 | 16 | 17 | def _validate_eth_checksum_address(addr: str): 18 | """Validate ETH type checksum address.""" 19 | addr = addr.replace("0x", "") 20 | addr_hash = keccak.new(addr.lower().encode("ascii")).digest().hex() # type: ignore 21 | 22 | if len(addr) != 40: 23 | return False 24 | 25 | for i in range(0, 40): 26 | if (int(addr_hash[i], 16) > 7 and addr[i].upper() != addr[i]) or ( 27 | int(addr_hash[i], 16) <= 7 and addr[i].lower() != addr[i] 28 | ): 29 | return False 30 | return True 31 | 32 | 33 | @validator 34 | def eth_address(value: str, /): 35 | """Return whether or not given value is a valid ethereum address. 36 | 37 | Full validation is implemented for ERC20 addresses. 38 | 39 | Examples: 40 | >>> eth_address('0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598') 41 | True 42 | >>> eth_address('0x8Ba1f109551bD432803012645Ac136ddd64DBa72') 43 | ValidationError(func=eth_address, args={'value': '0x8Ba1f109551bD432803012645Ac136ddd64DBa72'}) 44 | 45 | Args: 46 | value: 47 | Ethereum address string to validate. 48 | 49 | Returns: 50 | (Literal[True]): If `value` is a valid ethereum address. 51 | (ValidationError): If `value` is an invalid ethereum address. 52 | """ # noqa: E501 53 | if not _keccak_flag: 54 | raise ImportError( 55 | "Do `pip install validators[crypto-eth-addresses]` to perform `eth_address` validation." 56 | ) 57 | 58 | if not value: 59 | return False 60 | 61 | return re.compile(r"^0x[0-9a-f]{40}$|^0x[0-9A-F]{40}$").match( 62 | value 63 | ) or _validate_eth_checksum_address(value) 64 | -------------------------------------------------------------------------------- /src/validators/crypto_addresses/trx_address.py: -------------------------------------------------------------------------------- 1 | """TRX Address.""" 2 | 3 | # standard 4 | import hashlib 5 | import re 6 | 7 | # local 8 | from validators.utils import validator 9 | 10 | 11 | def _base58_decode(addr: str) -> bytes: 12 | """Decode a base58 encoded address.""" 13 | alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 14 | num = 0 15 | for char in addr: 16 | num = num * 58 + alphabet.index(char) 17 | return num.to_bytes(25, byteorder="big") 18 | 19 | 20 | def _validate_trx_checksum_address(addr: str) -> bool: 21 | """Validate TRX type checksum address.""" 22 | if len(addr) != 34: 23 | return False 24 | 25 | try: 26 | address = _base58_decode(addr) 27 | except ValueError: 28 | return False 29 | 30 | if len(address) != 25 or address[0] != 0x41: 31 | return False 32 | 33 | check_sum = hashlib.sha256(hashlib.sha256(address[:-4]).digest()).digest()[:4] 34 | return address[-4:] == check_sum 35 | 36 | 37 | @validator 38 | def trx_address(value: str, /): 39 | """Return whether or not given value is a valid tron address. 40 | 41 | Full validation is implemented for TRC20 tron addresses. 42 | 43 | Examples: 44 | >>> trx_address('TLjfbTbpZYDQ4EoA4N5CLNgGjfbF8ZWz38') 45 | True 46 | >>> trx_address('TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vd') 47 | ValidationError(func=trx_address, args={'value': 'TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vd'}) 48 | 49 | Args: 50 | value: 51 | Tron address string to validate. 52 | 53 | Returns: 54 | (Literal[True]): If `value` is a valid tron address. 55 | (ValidationError): If `value` is an invalid tron address. 56 | """ 57 | if not value: 58 | return False 59 | 60 | return re.compile(r"^[T][a-km-zA-HJ-NP-Z1-9]{33}$").match( 61 | value 62 | ) and _validate_trx_checksum_address(value) 63 | -------------------------------------------------------------------------------- /src/validators/domain.py: -------------------------------------------------------------------------------- 1 | """Domain.""" 2 | 3 | # standard 4 | from os import environ 5 | from pathlib import Path 6 | import re 7 | from typing import Optional, Set 8 | 9 | # local 10 | from .utils import validator 11 | 12 | 13 | class _IanaTLD: 14 | """Read IANA TLDs, and optionally cache them.""" 15 | 16 | _full_cache: Optional[Set[str]] = None 17 | # source: https://www.statista.com/statistics/265677 18 | _popular_cache = {"COM", "ORG", "RU", "DE", "NET", "BR", "UK", "JP", "FR", "IT"} 19 | _popular_cache.add("ONION") 20 | 21 | @classmethod 22 | def _retrieve(cls): 23 | with Path(__file__).parent.joinpath("_tld.txt").open() as tld_f: 24 | _ = next(tld_f) # ignore the first line 25 | for line in tld_f: 26 | yield line.strip() 27 | 28 | @classmethod 29 | def check(cls, tld: str): 30 | if tld in cls._popular_cache: 31 | return True 32 | if cls._full_cache is None: 33 | if environ.get("PYVLD_CACHE_TLD") == "True": 34 | cls._full_cache = set(cls._retrieve()) 35 | else: 36 | return tld in cls._retrieve() 37 | return tld in cls._full_cache 38 | 39 | 40 | @validator 41 | def domain( 42 | value: str, /, *, consider_tld: bool = False, rfc_1034: bool = False, rfc_2782: bool = False 43 | ): 44 | """Return whether or not given value is a valid domain. 45 | 46 | Examples: 47 | >>> domain('example.com') 48 | True 49 | >>> domain('example.com/') 50 | ValidationError(func=domain, args={'value': 'example.com/'}) 51 | >>> # Supports IDN domains as well:: 52 | >>> domain('xn----gtbspbbmkef.xn--p1ai') 53 | True 54 | 55 | Args: 56 | value: 57 | Domain string to validate. 58 | consider_tld: 59 | Restrict domain to TLDs allowed by IANA. 60 | rfc_1034: 61 | Allows optional trailing dot in the domain name. 62 | Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034). 63 | rfc_2782: 64 | Domain name is of type service record. 65 | Allows optional underscores in the domain name. 66 | Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782). 67 | 68 | 69 | Returns: 70 | (Literal[True]): If `value` is a valid domain name. 71 | (ValidationError): If `value` is an invalid domain name. 72 | 73 | Raises: 74 | (UnicodeError): If `value` cannot be encoded into `idna` or decoded into `utf-8`. 75 | """ 76 | if not value: 77 | return False 78 | 79 | if consider_tld and not _IanaTLD.check(value.rstrip(".").rsplit(".", 1)[-1].upper()): 80 | return False 81 | 82 | try: 83 | service_record = r"_" if rfc_2782 else "" 84 | trailing_dot = r"\.?$" if rfc_1034 else r"$" 85 | 86 | return not re.search(r"\s|__+", value) and re.match( 87 | # First character of the domain 88 | rf"^(?:[a-z0-9{service_record}]" 89 | # Sub-domain 90 | + rf"(?:[a-z0-9-{service_record}]{{0,61}}" 91 | # Hostname 92 | + rf"[a-z0-9{service_record}])?\.)" 93 | # First 61 characters of the gTLD 94 | + r"+[a-z0-9][a-z0-9-_]{0,61}" 95 | # Last character of the gTLD 96 | + rf"[a-z]{trailing_dot}", 97 | value.encode("idna").decode("utf-8"), 98 | re.IGNORECASE, 99 | ) 100 | except UnicodeError as err: 101 | raise UnicodeError(f"Unable to encode/decode {value}") from err 102 | -------------------------------------------------------------------------------- /src/validators/email.py: -------------------------------------------------------------------------------- 1 | """eMail.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .hostname import hostname 8 | from .utils import validator 9 | 10 | 11 | @validator 12 | def email( 13 | value: str, 14 | /, 15 | *, 16 | ipv6_address: bool = False, 17 | ipv4_address: bool = False, 18 | simple_host: bool = False, 19 | rfc_1034: bool = False, 20 | rfc_2782: bool = False, 21 | ): 22 | """Validate an email address. 23 | 24 | This was inspired from [Django's email validator][1]. 25 | Also ref: [RFC 1034][2], [RFC 5321][3] and [RFC 5322][4]. 26 | 27 | [1]: https://github.com/django/django/blob/main/django/core/validators.py#L174 28 | [2]: https://www.rfc-editor.org/rfc/rfc1034 29 | [3]: https://www.rfc-editor.org/rfc/rfc5321 30 | [4]: https://www.rfc-editor.org/rfc/rfc5322 31 | 32 | Examples: 33 | >>> email('someone@example.com') 34 | True 35 | >>> email('bogus@@') 36 | ValidationError(func=email, args={'value': 'bogus@@'}) 37 | 38 | Args: 39 | value: 40 | eMail string to validate. 41 | ipv6_address: 42 | When the domain part is an IPv6 address. 43 | ipv4_address: 44 | When the domain part is an IPv4 address. 45 | simple_host: 46 | When the domain part is a simple hostname. 47 | rfc_1034: 48 | Allow trailing dot in domain name. 49 | Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034). 50 | rfc_2782: 51 | Domain name is of type service record. 52 | Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782). 53 | 54 | Returns: 55 | (Literal[True]): If `value` is a valid eMail. 56 | (ValidationError): If `value` is an invalid eMail. 57 | """ 58 | if not value or value.count("@") != 1: 59 | return False 60 | 61 | username_part, domain_part = value.rsplit("@", 1) 62 | 63 | if len(username_part) > 64 or len(domain_part) > 253: 64 | # ref: RFC 1034 and 5231 65 | return False 66 | 67 | if ipv6_address or ipv4_address: 68 | if domain_part.startswith("[") and domain_part.endswith("]"): 69 | # ref: RFC 5321 70 | domain_part = domain_part.lstrip("[").rstrip("]") 71 | else: 72 | return False 73 | 74 | return ( 75 | bool( 76 | hostname( 77 | domain_part, 78 | skip_ipv6_addr=not ipv6_address, 79 | skip_ipv4_addr=not ipv4_address, 80 | may_have_port=False, 81 | maybe_simple=simple_host, 82 | rfc_1034=rfc_1034, 83 | rfc_2782=rfc_2782, 84 | ) 85 | ) 86 | if re.match( 87 | # extended latin 88 | r"(^[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF]" 89 | # dot-atom 90 | + r"|[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+" 91 | + r"(\.[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+)*$" 92 | # quoted-string 93 | + r'|^"(' 94 | + r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF\001-\010\013\014\016-\037" 95 | + r"!#-\[\]-\177]|\\[\011.]" 96 | + r')*")$', 97 | username_part, 98 | re.IGNORECASE, 99 | ) 100 | else False 101 | ) 102 | -------------------------------------------------------------------------------- /src/validators/encoding.py: -------------------------------------------------------------------------------- 1 | """Encoding.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | @validator 11 | def base16(value: str, /): 12 | """Return whether or not given value is a valid base16 encoding. 13 | 14 | Examples: 15 | >>> base16('a3f4b2') 16 | True 17 | >>> base16('a3f4Z1') 18 | ValidationError(func=base16, args={'value': 'a3f4Z1'}) 19 | 20 | Args: 21 | value: 22 | base16 string to validate. 23 | 24 | Returns: 25 | (Literal[True]): If `value` is a valid base16 encoding. 26 | (ValidationError): If `value` is an invalid base16 encoding. 27 | """ 28 | return re.match(r"^[0-9A-Fa-f]+$", value) if value else False 29 | 30 | 31 | @validator 32 | def base32(value: str, /): 33 | """Return whether or not given value is a valid base32 encoding. 34 | 35 | Examples: 36 | >>> base32('MFZWIZLTOQ======') 37 | True 38 | >>> base32('MfZW3zLT9Q======') 39 | ValidationError(func=base32, args={'value': 'MfZW3zLT9Q======'}) 40 | 41 | Args: 42 | value: 43 | base32 string to validate. 44 | 45 | Returns: 46 | (Literal[True]): If `value` is a valid base32 encoding. 47 | (ValidationError): If `value` is an invalid base32 encoding. 48 | """ 49 | return re.match(r"^[A-Z2-7]+=*$", value) if value else False 50 | 51 | 52 | @validator 53 | def base58(value: str, /): 54 | """Return whether or not given value is a valid base58 encoding. 55 | 56 | Examples: 57 | >>> base58('14pq6y9H2DLGahPsM4s7ugsNSD2uxpHsJx') 58 | True 59 | >>> base58('cUSECm5YzcXJwP') 60 | True 61 | 62 | Args: 63 | value: 64 | base58 string to validate. 65 | 66 | Returns: 67 | (Literal[True]): If `value` is a valid base58 encoding. 68 | (ValidationError): If `value` is an invalid base58 encoding. 69 | """ 70 | return re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", value) if value else False 71 | 72 | 73 | @validator 74 | def base64(value: str, /): 75 | """Return whether or not given value is a valid base64 encoding. 76 | 77 | Examples: 78 | >>> base64('Y2hhcmFjdGVyIHNldA==') 79 | True 80 | >>> base64('cUSECm5YzcXJwP') 81 | ValidationError(func=base64, args={'value': 'cUSECm5YzcXJwP'}) 82 | 83 | Args: 84 | value: 85 | base64 string to validate. 86 | 87 | Returns: 88 | (Literal[True]): If `value` is a valid base64 encoding. 89 | (ValidationError): If `value` is an invalid base64 encoding. 90 | """ 91 | return ( 92 | re.match(r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", value) 93 | if value 94 | else False 95 | ) 96 | -------------------------------------------------------------------------------- /src/validators/finance.py: -------------------------------------------------------------------------------- 1 | """Finance.""" 2 | 3 | from .utils import validator 4 | 5 | 6 | def _cusip_checksum(cusip: str): 7 | check, val = 0, None 8 | 9 | for idx in range(9): 10 | c = cusip[idx] 11 | if c >= "0" and c <= "9": 12 | val = ord(c) - ord("0") 13 | elif c >= "A" and c <= "Z": 14 | val = 10 + ord(c) - ord("A") 15 | elif c >= "a" and c <= "z": 16 | val = 10 + ord(c) - ord("a") 17 | elif c == "*": 18 | val = 36 19 | elif c == "@": 20 | val = 37 21 | elif c == "#": 22 | val = 38 23 | else: 24 | return False 25 | 26 | if idx & 1: 27 | val += val 28 | 29 | check = check + (val // 10) + (val % 10) 30 | 31 | return (check % 10) == 0 32 | 33 | 34 | def _isin_checksum(value: str): 35 | check, val = 0, None 36 | 37 | for idx in range(12): 38 | c = value[idx] 39 | if c >= "0" and c <= "9" and idx > 1: 40 | val = ord(c) - ord("0") 41 | elif c >= "A" and c <= "Z": 42 | val = 10 + ord(c) - ord("A") 43 | elif c >= "a" and c <= "z": 44 | val = 10 + ord(c) - ord("a") 45 | else: 46 | return False 47 | 48 | if idx & 1: 49 | val += val 50 | 51 | return (check % 10) == 0 52 | 53 | 54 | @validator 55 | def cusip(value: str): 56 | """Return whether or not given value is a valid CUSIP. 57 | 58 | Checks if the value is a valid [CUSIP][1]. 59 | [1]: https://en.wikipedia.org/wiki/CUSIP 60 | 61 | Examples: 62 | >>> cusip('037833DP2') 63 | True 64 | >>> cusip('037833DP3') 65 | ValidationError(func=cusip, args={'value': '037833DP3'}) 66 | 67 | Args: 68 | value: CUSIP string to validate. 69 | 70 | Returns: 71 | (Literal[True]): If `value` is a valid CUSIP string. 72 | (ValidationError): If `value` is an invalid CUSIP string. 73 | """ 74 | return len(value) == 9 and _cusip_checksum(value) 75 | 76 | 77 | @validator 78 | def isin(value: str): 79 | """Return whether or not given value is a valid ISIN. 80 | 81 | Checks if the value is a valid [ISIN][1]. 82 | [1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number 83 | 84 | Examples: 85 | >>> isin('037833DP2') 86 | ValidationError(func=isin, args={'value': '037833DP2'}) 87 | >>> isin('037833DP3') 88 | ValidationError(func=isin, args={'value': '037833DP3'}) 89 | 90 | Args: 91 | value: ISIN string to validate. 92 | 93 | Returns: 94 | (Literal[True]): If `value` is a valid ISIN string. 95 | (ValidationError): If `value` is an invalid ISIN string. 96 | """ 97 | return len(value) == 12 and _isin_checksum(value) 98 | 99 | 100 | @validator 101 | def sedol(value: str): 102 | """Return whether or not given value is a valid SEDOL. 103 | 104 | Checks if the value is a valid [SEDOL][1]. 105 | [1]: https://en.wikipedia.org/wiki/SEDOL 106 | 107 | Examples: 108 | >>> sedol('2936921') 109 | True 110 | >>> sedol('29A6922') 111 | ValidationError(func=sedol, args={'value': '29A6922'}) 112 | 113 | Args: 114 | value: SEDOL string to validate. 115 | 116 | Returns: 117 | (Literal[True]): If `value` is a valid SEDOL string. 118 | (ValidationError): If `value` is an invalid SEDOL string. 119 | """ 120 | if len(value) != 7: 121 | return False 122 | 123 | weights = [1, 3, 1, 7, 3, 9, 1] 124 | check = 0 125 | for idx in range(7): 126 | c = value[idx] 127 | if c in "AEIOU": 128 | return False 129 | 130 | val = None 131 | if c >= "0" and c <= "9": 132 | val = ord(c) - ord("0") 133 | elif c >= "A" and c <= "Z": 134 | val = 10 + ord(c) - ord("A") 135 | else: 136 | return False 137 | check += val * weights[idx] 138 | 139 | return (check % 10) == 0 140 | -------------------------------------------------------------------------------- /src/validators/hashes.py: -------------------------------------------------------------------------------- 1 | """Hashes.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | @validator 11 | def md5(value: str, /): 12 | """Return whether or not given value is a valid MD5 hash. 13 | 14 | Examples: 15 | >>> md5('d41d8cd98f00b204e9800998ecf8427e') 16 | True 17 | >>> md5('900zz11') 18 | ValidationError(func=md5, args={'value': '900zz11'}) 19 | 20 | Args: 21 | value: 22 | MD5 string to validate. 23 | 24 | Returns: 25 | (Literal[True]): If `value` is a valid MD5 hash. 26 | (ValidationError): If `value` is an invalid MD5 hash. 27 | """ 28 | return re.match(r"^[0-9a-f]{32}$", value, re.IGNORECASE) if value else False 29 | 30 | 31 | @validator 32 | def sha1(value: str, /): 33 | """Return whether or not given value is a valid SHA1 hash. 34 | 35 | Examples: 36 | >>> sha1('da39a3ee5e6b4b0d3255bfef95601890afd80709') 37 | True 38 | >>> sha1('900zz11') 39 | ValidationError(func=sha1, args={'value': '900zz11'}) 40 | 41 | Args: 42 | value: 43 | SHA1 string to validate. 44 | 45 | Returns: 46 | (Literal[True]): If `value` is a valid SHA1 hash. 47 | (ValidationError): If `value` is an invalid SHA1 hash. 48 | """ 49 | return re.match(r"^[0-9a-f]{40}$", value, re.IGNORECASE) if value else False 50 | 51 | 52 | @validator 53 | def sha224(value: str, /): 54 | """Return whether or not given value is a valid SHA224 hash. 55 | 56 | Examples: 57 | >>> sha224('d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f') 58 | True 59 | >>> sha224('900zz11') 60 | ValidationError(func=sha224, args={'value': '900zz11'}) 61 | 62 | Args: 63 | value: 64 | SHA224 string to validate. 65 | 66 | Returns: 67 | (Literal[True]): If `value` is a valid SHA224 hash. 68 | (ValidationError): If `value` is an invalid SHA224 hash. 69 | """ 70 | return re.match(r"^[0-9a-f]{56}$", value, re.IGNORECASE) if value else False 71 | 72 | 73 | @validator 74 | def sha256(value: str, /): 75 | """Return whether or not given value is a valid SHA256 hash. 76 | 77 | Examples: 78 | >>> sha256( 79 | ... 'e3b0c44298fc1c149afbf4c8996fb924' 80 | ... '27ae41e4649b934ca495991b7852b855' 81 | ... ) 82 | True 83 | >>> sha256('900zz11') 84 | ValidationError(func=sha256, args={'value': '900zz11'}) 85 | 86 | Args: 87 | value: 88 | SHA256 string to validate. 89 | 90 | Returns: 91 | (Literal[True]): If `value` is a valid SHA256 hash. 92 | (ValidationError): If `value` is an invalid SHA256 hash. 93 | """ 94 | return re.match(r"^[0-9a-f]{64}$", value, re.IGNORECASE) if value else False 95 | 96 | 97 | @validator 98 | def sha384(value: str, /): 99 | """Return whether or not given value is a valid SHA384 hash. 100 | 101 | Examples: 102 | >>> sha384( 103 | ... 'cb00753f45a35e8bb5a03d699ac65007272c32ab0eded163' 104 | ... '1a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7' 105 | ... ) 106 | True 107 | >>> sha384('900zz11') 108 | ValidationError(func=sha384, args={'value': '900zz11'}) 109 | 110 | Args: 111 | value: 112 | SHA384 string to validate. 113 | 114 | Returns: 115 | (Literal[True]): If `value` is a valid SHA384 hash. 116 | (ValidationError): If `value` is an invalid SHA384 hash. 117 | """ 118 | return re.match(r"^[0-9a-f]{96}$", value, re.IGNORECASE) if value else False 119 | 120 | 121 | @validator 122 | def sha512(value: str, /): 123 | """Return whether or not given value is a valid SHA512 hash. 124 | 125 | Examples: 126 | >>> sha512( 127 | ... 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce' 128 | ... '9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af9' 129 | ... '27da3e' 130 | ... ) 131 | True 132 | >>> sha512('900zz11') 133 | ValidationError(func=sha512, args={'value': '900zz11'}) 134 | 135 | Args: 136 | value: 137 | SHA512 string to validate. 138 | 139 | Returns: 140 | (Literal[True]): If `value` is a valid SHA512 hash. 141 | (ValidationError): If `value` is an invalid SHA512 hash. 142 | """ 143 | return re.match(r"^[0-9a-f]{128}$", value, re.IGNORECASE) if value else False 144 | -------------------------------------------------------------------------------- /src/validators/hostname.py: -------------------------------------------------------------------------------- 1 | """Hostname.""" 2 | 3 | # standard 4 | from functools import lru_cache 5 | import re 6 | from typing import Optional 7 | 8 | from .domain import domain 9 | 10 | # local 11 | from .ip_address import ipv4, ipv6 12 | from .utils import validator 13 | 14 | 15 | @lru_cache 16 | def _port_regex(): 17 | """Port validation regex.""" 18 | return re.compile( 19 | r"^\:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|" 20 | + r"6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3})$", 21 | ) 22 | 23 | 24 | @lru_cache 25 | def _simple_hostname_regex(): 26 | """Simple hostname validation regex.""" 27 | # {0,59} because two characters are already matched at 28 | # the beginning and at the end, making the range {1, 61} 29 | return re.compile(r"^(?!-)[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?(?>> hostname("ubuntu-pc:443") 67 | True 68 | >>> hostname("this-pc") 69 | True 70 | >>> hostname("xn----gtbspbbmkef.xn--p1ai:65535") 71 | True 72 | >>> hostname("_example.com") 73 | ValidationError(func=hostname, args={'value': '_example.com'}) 74 | >>> hostname("123.5.77.88:31000") 75 | True 76 | >>> hostname("12.12.12.12") 77 | True 78 | >>> hostname("[::1]:22") 79 | True 80 | >>> hostname("dead:beef:0:0:0:0000:42:1") 81 | True 82 | >>> hostname("[0:0:0:0:0:ffff:1.2.3.4]:-65538") 83 | ValidationError(func=hostname, args={'value': '[0:0:0:0:0:ffff:1.2.3.4]:-65538'}) 84 | >>> hostname("[0:&:b:c:@:e:f::]:9999") 85 | ValidationError(func=hostname, args={'value': '[0:&:b:c:@:e:f::]:9999'}) 86 | 87 | Args: 88 | value: 89 | Hostname string to validate. 90 | skip_ipv6_addr: 91 | When hostname string cannot be an IPv6 address. 92 | skip_ipv4_addr: 93 | When hostname string cannot be an IPv4 address. 94 | may_have_port: 95 | Hostname string may contain port number. 96 | maybe_simple: 97 | Hostname string maybe only hyphens and alpha-numerals. 98 | consider_tld: 99 | Restrict domain to TLDs allowed by IANA. 100 | private: 101 | Embedded IP address is public if `False`, private/local if `True`. 102 | rfc_1034: 103 | Allow trailing dot in domain/host name. 104 | Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034). 105 | rfc_2782: 106 | Domain/Host name is of type service record. 107 | Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782). 108 | 109 | Returns: 110 | (Literal[True]): If `value` is a valid hostname. 111 | (ValidationError): If `value` is an invalid hostname. 112 | """ 113 | if not value: 114 | return False 115 | 116 | if may_have_port and (host_seg := _port_validator(value)): 117 | return ( 118 | (_simple_hostname_regex().match(host_seg) if maybe_simple else False) 119 | or domain(host_seg, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) 120 | or (False if skip_ipv4_addr else ipv4(host_seg, cidr=False, private=private)) 121 | or (False if skip_ipv6_addr else ipv6(host_seg, cidr=False)) 122 | ) 123 | 124 | return ( 125 | (_simple_hostname_regex().match(value) if maybe_simple else False) 126 | or domain(value, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) 127 | or (False if skip_ipv4_addr else ipv4(value, cidr=False, private=private)) 128 | or (False if skip_ipv6_addr else ipv6(value, cidr=False)) 129 | ) 130 | -------------------------------------------------------------------------------- /src/validators/i18n/__init__.py: -------------------------------------------------------------------------------- 1 | """i18n.""" 2 | 3 | # local 4 | from .es import es_cif, es_doi, es_nie, es_nif 5 | from .fi import fi_business_id, fi_ssn 6 | from .fr import fr_department, fr_ssn 7 | from .ind import ind_aadhar, ind_pan 8 | from .ru import ru_inn 9 | 10 | __all__ = ( 11 | "fi_business_id", 12 | "fi_ssn", 13 | "es_cif", 14 | "es_doi", 15 | "es_nie", 16 | "es_nif", 17 | "fr_department", 18 | "fr_ssn", 19 | "ind_aadhar", 20 | "ind_pan", 21 | "ru_inn", 22 | ) 23 | -------------------------------------------------------------------------------- /src/validators/i18n/es.py: -------------------------------------------------------------------------------- 1 | """Spain.""" 2 | 3 | # standard 4 | from typing import Dict 5 | 6 | # local 7 | from validators.utils import validator 8 | 9 | 10 | def _nif_nie_validation(value: str, number_by_letter: Dict[str, str]): 11 | """Validate if the doi is a NIF or a NIE.""" 12 | if len(value) != 9: 13 | return False 14 | value = value.upper() 15 | table = "TRWAGMYFPDXBNJZSQVHLCKE" 16 | # If it is not a DNI, convert the first 17 | # letter to the corresponding digit 18 | numbers = number_by_letter.get(value[0], value[0]) + value[1:8] 19 | # doi[8] is control 20 | return numbers.isdigit() and value[8] == table[int(numbers) % 23] 21 | 22 | 23 | @validator 24 | def es_cif(value: str, /): 25 | """Validate a Spanish CIF. 26 | 27 | Each company in Spain prior to 2008 had a distinct CIF and has been 28 | discontinued. For more information see [wikipedia.org/cif][1]. 29 | 30 | The new replacement is to use NIF for absolutely everything. The issue is 31 | that there are "types" of NIFs now: company, person [citizen or resident] 32 | all distinguished by the first character of the DOI. For this reason we 33 | will continue to call CIFs NIFs, that are used for companies. 34 | 35 | This validator is based on [generadordni.es][2]. 36 | 37 | [1]: https://es.wikipedia.org/wiki/C%C3%B3digo_de_identificaci%C3%B3n_fiscal 38 | [2]: https://generadordni.es/ 39 | 40 | Examples: 41 | >>> es_cif('B25162520') 42 | True 43 | >>> es_cif('B25162529') 44 | ValidationError(func=es_cif, args={'value': 'B25162529'}) 45 | 46 | Args: 47 | value: 48 | DOI string which is to be validated. 49 | 50 | Returns: 51 | (Literal[True]): If `value` is a valid DOI string. 52 | (ValidationError): If `value` is an invalid DOI string. 53 | """ 54 | if not value or len(value) != 9: 55 | return False 56 | value = value.upper() 57 | table = "JABCDEFGHI" 58 | first_chr = value[0] 59 | doi_body = value[1:8] 60 | control = value[8] 61 | if not doi_body.isdigit(): 62 | return False 63 | res = ( 64 | 10 65 | - sum( 66 | # Multiply each positionally even doi 67 | # digit by 2 and sum it all together 68 | sum(map(int, str(int(char) * 2))) if index % 2 == 0 else int(char) 69 | for index, char in enumerate(doi_body) 70 | ) 71 | % 10 72 | ) % 10 73 | if first_chr in "ABEH": # Number type 74 | return str(res) == control 75 | if first_chr in "PSQW": # Letter type 76 | return table[res] == control 77 | return control in {str(res), table[res]} if first_chr in "CDFGJNRUV" else False 78 | 79 | 80 | @validator 81 | def es_nif(value: str, /): 82 | """Validate a Spanish NIF. 83 | 84 | Each entity, be it person or company in Spain has a distinct NIF. Since 85 | we've designated CIF to be a company NIF, this NIF is only for person. 86 | For more information see [wikipedia.org/nif][1]. This validator 87 | is based on [generadordni.es][2]. 88 | 89 | [1]: https://es.wikipedia.org/wiki/N%C3%BAmero_de_identificaci%C3%B3n_fiscal 90 | [2]: https://generadordni.es/ 91 | 92 | Examples: 93 | >>> es_nif('26643189N') 94 | True 95 | >>> es_nif('26643189X') 96 | ValidationError(func=es_nif, args={'value': '26643189X'}) 97 | 98 | Args: 99 | value: 100 | DOI string which is to be validated. 101 | 102 | Returns: 103 | (Literal[True]): If `value` is a valid DOI string. 104 | (ValidationError): If `value` is an invalid DOI string. 105 | """ 106 | number_by_letter = {"L": "0", "M": "0", "K": "0"} 107 | return _nif_nie_validation(value, number_by_letter) 108 | 109 | 110 | @validator 111 | def es_nie(value: str, /): 112 | """Validate a Spanish NIE. 113 | 114 | The NIE is a tax identification number in Spain, known in Spanish 115 | as the NIE, or more formally the Número de identidad de extranjero. 116 | For more information see [wikipedia.org/nie][1]. This validator 117 | is based on [generadordni.es][2]. 118 | 119 | [1]: https://es.wikipedia.org/wiki/N%C3%BAmero_de_identidad_de_extranjero 120 | [2]: https://generadordni.es/ 121 | 122 | Examples: 123 | >>> es_nie('X0095892M') 124 | True 125 | >>> es_nie('X0095892X') 126 | ValidationError(func=es_nie, args={'value': 'X0095892X'}) 127 | 128 | Args: 129 | value: 130 | DOI string which is to be validated. 131 | 132 | Returns: 133 | (Literal[True]): If `value` is a valid DOI string. 134 | (ValidationError): If `value` is an invalid DOI string. 135 | """ 136 | number_by_letter = {"X": "0", "Y": "1", "Z": "2"} 137 | # NIE must must start with X Y or Z 138 | if value and value[0] in number_by_letter: 139 | return _nif_nie_validation(value, number_by_letter) 140 | return False 141 | 142 | 143 | @validator 144 | def es_doi(value: str, /): 145 | """Validate a Spanish DOI. 146 | 147 | A DOI in spain is all NIF / CIF / NIE / DNI -- a digital ID. 148 | For more information see [wikipedia.org/doi][1]. This validator 149 | is based on [generadordni.es][2]. 150 | 151 | [1]: https://es.wikipedia.org/wiki/Identificador_de_objeto_digital 152 | [2]: https://generadordni.es/ 153 | 154 | Examples: 155 | >>> es_doi('X0095892M') 156 | True 157 | >>> es_doi('X0095892X') 158 | ValidationError(func=es_doi, args={'value': 'X0095892X'}) 159 | 160 | Args: 161 | value: 162 | DOI string which is to be validated. 163 | 164 | Returns: 165 | (Literal[True]): If `value` is a valid DOI string. 166 | (ValidationError): If `value` is an invalid DOI string. 167 | """ 168 | return es_nie(value) or es_nif(value) or es_cif(value) 169 | -------------------------------------------------------------------------------- /src/validators/i18n/fi.py: -------------------------------------------------------------------------------- 1 | """Finland.""" 2 | 3 | # standard 4 | from functools import lru_cache 5 | import re 6 | 7 | # local 8 | from validators.utils import validator 9 | 10 | 11 | @lru_cache 12 | def _business_id_pattern(): 13 | """Business ID Pattern.""" 14 | return re.compile(r"^[0-9]{7}-[0-9]$") 15 | 16 | 17 | @lru_cache 18 | def _ssn_pattern(ssn_check_marks: str): 19 | """SSN Pattern.""" 20 | return re.compile( 21 | r"""^ 22 | (?P(0[1-9]|[1-2]\d|3[01]) 23 | (0[1-9]|1[012]) 24 | (\d{{2}})) 25 | [ABCDEFYXWVU+-] 26 | (?P(\d{{3}})) 27 | (?P[{check_marks}])$""".format(check_marks=ssn_check_marks), 28 | re.VERBOSE, 29 | ) 30 | 31 | 32 | @validator 33 | def fi_business_id(value: str, /): 34 | """Validate a Finnish Business ID. 35 | 36 | Each company in Finland has a distinct business id. For more 37 | information see [Finnish Trade Register][1] 38 | 39 | [1]: http://en.wikipedia.org/wiki/Finnish_Trade_Register 40 | 41 | Examples: 42 | >>> fi_business_id('0112038-9') # Fast Monkeys Ltd 43 | True 44 | >>> fi_business_id('1234567-8') # Bogus ID 45 | ValidationError(func=fi_business_id, args={'value': '1234567-8'}) 46 | 47 | Args: 48 | value: 49 | Business ID string to be validated. 50 | 51 | Returns: 52 | (Literal[True]): If `value` is a valid finnish business id. 53 | (ValidationError): If `value` is an invalid finnish business id. 54 | """ 55 | if not value: 56 | return False 57 | if not re.match(_business_id_pattern(), value): 58 | return False 59 | factors = [7, 9, 10, 5, 8, 4, 2] 60 | numbers = map(int, value[:7]) 61 | checksum = int(value[8]) 62 | modulo = sum(f * n for f, n in zip(factors, numbers)) % 11 63 | return (11 - modulo == checksum) or (modulo == checksum == 0) 64 | 65 | 66 | @validator 67 | def fi_ssn(value: str, /, *, allow_temporal_ssn: bool = True): 68 | """Validate a Finnish Social Security Number. 69 | 70 | This validator is based on [django-localflavor-fi][1]. 71 | 72 | [1]: https://github.com/django/django-localflavor-fi/ 73 | 74 | Examples: 75 | >>> fi_ssn('010101-0101') 76 | True 77 | >>> fi_ssn('101010-0102') 78 | ValidationError(func=fi_ssn, args={'value': '101010-0102'}) 79 | 80 | Args: 81 | value: 82 | Social Security Number to be validated. 83 | allow_temporal_ssn: 84 | Whether to accept temporal SSN numbers. Temporal SSN numbers are the 85 | ones where the serial is in the range [900-999]. By default temporal 86 | SSN numbers are valid. 87 | 88 | Returns: 89 | (Literal[True]): If `value` is a valid finnish SSN. 90 | (ValidationError): If `value` is an invalid finnish SSN. 91 | """ 92 | if not value: 93 | return False 94 | ssn_check_marks = "0123456789ABCDEFHJKLMNPRSTUVWXY" 95 | if not (result := re.match(_ssn_pattern(ssn_check_marks), value)): 96 | return False 97 | gd = result.groupdict() 98 | checksum = int(gd["date"] + gd["serial"]) 99 | return ( 100 | int(gd["serial"]) >= 2 101 | and (allow_temporal_ssn or int(gd["serial"]) <= 899) 102 | and ssn_check_marks[checksum % len(ssn_check_marks)] == gd["checksum"] 103 | ) 104 | -------------------------------------------------------------------------------- /src/validators/i18n/fr.py: -------------------------------------------------------------------------------- 1 | """France.""" 2 | 3 | # standard 4 | from functools import lru_cache 5 | import re 6 | import typing 7 | 8 | # local 9 | from validators.utils import validator 10 | 11 | 12 | @lru_cache 13 | def _ssn_pattern(): 14 | """SSN Pattern.""" 15 | return re.compile( 16 | r"^([1,2])" # gender (1=M, 2=F) 17 | r"\s(\d{2})" # year of birth 18 | r"\s(0[1-9]|1[0-2])" # month of birth 19 | r"\s(\d{2,3}|2[A,B])" # department of birth 20 | r"\s(\d{2,3})" # town of birth 21 | r"\s(\d{3})" # registration number 22 | r"(?:\s(\d{2}))?$", # control key (may or may not be provided) 23 | re.VERBOSE, 24 | ) 25 | 26 | 27 | @validator 28 | def fr_department(value: typing.Union[str, int]): 29 | """Validate a french department number. 30 | 31 | Examples: 32 | >>> fr_department(20) # can be an integer 33 | ValidationError(func=fr_department, args={'value': 20}) 34 | >>> fr_department("20") 35 | ValidationError(func=fr_department, args={'value': '20'}) 36 | >>> fr_department("971") # Guadeloupe 37 | True 38 | >>> fr_department("00") 39 | ValidationError(func=fr_department, args={'value': '00'}) 40 | >>> fr_department('2A') # Corsica 41 | True 42 | >>> fr_department('2B') 43 | True 44 | >>> fr_department('2C') 45 | ValidationError(func=fr_department, args={'value': '2C'}) 46 | 47 | Args: 48 | value: 49 | French department number to validate. 50 | 51 | Returns: 52 | (Literal[True]): If `value` is a valid french department number. 53 | (ValidationError): If `value` is an invalid french department number. 54 | """ 55 | if not value: 56 | return False 57 | if isinstance(value, str): 58 | if value in ("2A", "2B"): # Corsica 59 | return True 60 | try: 61 | value = int(value) 62 | except ValueError: 63 | return False 64 | return 1 <= value <= 19 or 21 <= value <= 95 or 971 <= value <= 976 # Overseas departments 65 | 66 | 67 | @validator 68 | def fr_ssn(value: str): 69 | """Validate a french Social Security Number. 70 | 71 | Each french citizen has a distinct Social Security Number. 72 | For more information see [French Social Security Number][1] (sadly unavailable in english). 73 | 74 | [1]: https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France 75 | 76 | Examples: 77 | >>> fr_ssn('1 84 12 76 451 089 46') 78 | True 79 | >>> fr_ssn('1 84 12 76 451 089') # control key is optional 80 | True 81 | >>> fr_ssn('3 84 12 76 451 089 46') # wrong gender number 82 | ValidationError(func=fr_ssn, args={'value': '3 84 12 76 451 089 46'}) 83 | >>> fr_ssn('1 84 12 76 451 089 47') # wrong control key 84 | ValidationError(func=fr_ssn, args={'value': '1 84 12 76 451 089 47'}) 85 | 86 | Args: 87 | value: 88 | French Social Security Number string to validate. 89 | 90 | Returns: 91 | (Literal[True]): If `value` is a valid french Social Security Number. 92 | (ValidationError): If `value` is an invalid french Social Security Number. 93 | """ 94 | if not value: 95 | return False 96 | matched = re.match(_ssn_pattern(), value) 97 | if not matched: 98 | return False 99 | groups = list(matched.groups()) 100 | control_key = groups[-1] 101 | department = groups[3] 102 | if department != "99" and not fr_department(department): 103 | # 99 stands for foreign born people 104 | return False 105 | if control_key is None: 106 | # no control key provided, no additional check needed 107 | return True 108 | if len(department) == len(groups[4]): 109 | # if the department number is 3 digits long (overseas departments), 110 | # the town number must be 2 digits long 111 | # and vice versa 112 | return False 113 | if department in ("2A", "2B"): 114 | # Corsica's department numbers are not in the same range as the others 115 | # thus 2A and 2B are replaced by 19 and 18 respectively to compute the control key 116 | groups[3] = "19" if department == "2A" else "18" 117 | # the control key is valid if it is equal to 97 - (the first 13 digits modulo 97) 118 | digits = int("".join(groups[:-1])) 119 | return int(control_key) == (97 - (digits % 97)) 120 | -------------------------------------------------------------------------------- /src/validators/i18n/ind.py: -------------------------------------------------------------------------------- 1 | """India.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from validators.utils import validator 8 | 9 | 10 | @validator 11 | def ind_aadhar(value: str): 12 | """Validate an indian aadhar card number. 13 | 14 | Examples: 15 | >>> ind_aadhar('3675 9834 6015') 16 | True 17 | >>> ind_aadhar('3675 ABVC 2133') 18 | ValidationError(func=ind_aadhar, args={'value': '3675 ABVC 2133'}) 19 | 20 | Args: 21 | value: Aadhar card number string to validate. 22 | 23 | Returns: 24 | (Literal[True]): If `value` is a valid aadhar card number. 25 | (ValidationError): If `value` is an invalid aadhar card number. 26 | """ 27 | return re.match(r"^[2-9]{1}\d{3}\s\d{4}\s\d{4}$", value) 28 | 29 | 30 | @validator 31 | def ind_pan(value: str): 32 | """Validate a pan card number. 33 | 34 | Examples: 35 | >>> ind_pan('ABCDE9999K') 36 | True 37 | >>> ind_pan('ABC5d7896B') 38 | ValidationError(func=ind_pan, args={'value': 'ABC5d7896B'}) 39 | 40 | Args: 41 | value: PAN card number string to validate. 42 | 43 | Returns: 44 | (Literal[True]): If `value` is a valid PAN card number. 45 | (ValidationError): If `value` is an invalid PAN card number. 46 | """ 47 | return re.match(r"[A-Z]{5}\d{4}[A-Z]{1}", value) 48 | -------------------------------------------------------------------------------- /src/validators/i18n/ru.py: -------------------------------------------------------------------------------- 1 | """Russia.""" 2 | 3 | from validators.utils import validator 4 | 5 | 6 | @validator 7 | def ru_inn(value: str): 8 | """Validate a Russian INN (Taxpayer Identification Number). 9 | 10 | The INN can be either 10 digits (for companies) or 12 digits (for individuals). 11 | The function checks both the length and the control digits according to Russian tax rules. 12 | 13 | Examples: 14 | >>> ru_inn('500100732259') # Valid 12-digit INN 15 | True 16 | >>> ru_inn('7830002293') # Valid 10-digit INN 17 | True 18 | >>> ru_inn('1234567890') # Invalid INN 19 | ValidationError(func=ru_inn, args={'value': '1234567890'}) 20 | 21 | Args: 22 | value: Russian INN string to validate. Can contain only digits. 23 | 24 | Returns: 25 | (Literal[True]): If `value` is a valid Russian INN. 26 | (ValidationError): If `value` is an invalid Russian INN. 27 | 28 | Note: 29 | The validation follows the official algorithm: 30 | - For 10-digit INN: checks 10th control digit 31 | - For 12-digit INN: checks both 11th and 12th control digits 32 | """ 33 | if not value: 34 | return False 35 | 36 | try: 37 | digits = list(map(int, value)) 38 | # company 39 | if len(digits) == 10: 40 | weight_coefs = [2, 4, 10, 3, 5, 9, 4, 6, 8, 0] 41 | control_number = sum([d * w for d, w in zip(digits, weight_coefs)]) % 11 42 | return ( 43 | (control_number % 10) == digits[-1] 44 | if control_number > 9 45 | else control_number == digits[-1] 46 | ) 47 | # person 48 | elif len(digits) == 12: 49 | weight_coefs1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0, 0] 50 | control_number1 = sum([d * w for d, w in zip(digits, weight_coefs1)]) % 11 51 | weight_coefs2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0] 52 | control_number2 = sum([d * w for d, w in zip(digits, weight_coefs2)]) % 11 53 | return ( 54 | (control_number1 % 10) == digits[-2] 55 | if control_number1 > 9 56 | else control_number1 == digits[-2] and (control_number2 % 10) == digits[-1] 57 | if control_number2 > 9 58 | else control_number2 == digits[-1] 59 | ) 60 | else: 61 | return False 62 | except ValueError: 63 | return False 64 | -------------------------------------------------------------------------------- /src/validators/iban.py: -------------------------------------------------------------------------------- 1 | """IBAN.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | def _char_value(char: str): 11 | """A=10, B=11, ..., Z=35.""" 12 | return char if char.isdigit() else str(10 + ord(char) - ord("A")) 13 | 14 | 15 | def _mod_check(value: str): 16 | """Check if the value string passes the mod97-test.""" 17 | # move country code and check numbers to end 18 | rearranged = value[4:] + value[:4] 19 | return int("".join(_char_value(char) for char in rearranged)) % 97 == 1 20 | 21 | 22 | @validator 23 | def iban(value: str, /): 24 | """Return whether or not given value is a valid IBAN code. 25 | 26 | Examples: 27 | >>> iban('DE29100500001061045672') 28 | True 29 | >>> iban('123456') 30 | ValidationError(func=iban, args={'value': '123456'}) 31 | 32 | Args: 33 | value: 34 | IBAN string to validate. 35 | 36 | Returns: 37 | (Literal[True]): If `value` is a valid IBAN code. 38 | (ValidationError): If `value` is an invalid IBAN code. 39 | """ 40 | return ( 41 | (re.match(r"^[a-z]{2}[0-9]{2}[a-z0-9]{11,30}$", value, re.IGNORECASE) and _mod_check(value)) 42 | if value 43 | else False 44 | ) 45 | -------------------------------------------------------------------------------- /src/validators/ip_address.py: -------------------------------------------------------------------------------- 1 | """IP Address.""" 2 | 3 | # standard 4 | from ipaddress import ( 5 | AddressValueError, 6 | IPv4Address, 7 | IPv4Network, 8 | IPv6Address, 9 | IPv6Network, 10 | NetmaskValueError, 11 | ) 12 | import re 13 | from typing import Optional 14 | 15 | # local 16 | from .utils import validator 17 | 18 | 19 | def _check_private_ip(value: str, is_private: Optional[bool]): 20 | if is_private is None: 21 | return True 22 | if ( 23 | any( 24 | value.startswith(l_bit) 25 | for l_bit in { 26 | "10.", # private 27 | "192.168.", # private 28 | "169.254.", # link-local 29 | "127.", # localhost 30 | "0.0.0.0", # loopback #nosec 31 | } 32 | ) 33 | or re.match(r"^172\.(?:1[6-9]|2\d|3[0-1])\.", value) # private 34 | or re.match(r"^(?:22[4-9]|23[0-9]|24[0-9]|25[0-5])\.", value) # broadcast 35 | ): 36 | return is_private 37 | 38 | return not is_private 39 | 40 | 41 | @validator 42 | def ipv4( 43 | value: str, 44 | /, 45 | *, 46 | cidr: bool = True, 47 | strict: bool = False, 48 | private: Optional[bool] = None, 49 | host_bit: bool = True, 50 | ): 51 | """Returns whether a given value is a valid IPv4 address. 52 | 53 | From Python version 3.9.5 leading zeros are no longer tolerated 54 | and are treated as an error. The initial version of ipv4 validator 55 | was inspired from [WTForms IPAddress validator][1]. 56 | 57 | [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py 58 | 59 | Examples: 60 | >>> ipv4('123.0.0.7') 61 | True 62 | >>> ipv4('1.1.1.1/8') 63 | True 64 | >>> ipv4('900.80.70.11') 65 | ValidationError(func=ipv4, args={'value': '900.80.70.11'}) 66 | 67 | Args: 68 | value: 69 | IP address string to validate. 70 | cidr: 71 | IP address string may contain CIDR notation. 72 | strict: 73 | IP address string is strictly in CIDR notation. 74 | private: 75 | IP address is public if `False`, private/local/loopback/broadcast if `True`. 76 | host_bit: 77 | If `False` and host bits (along with network bits) _are_ set in the supplied 78 | address, this function raises a validation error. ref [IPv4Network][2]. 79 | [2]: https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Network 80 | 81 | Returns: 82 | (Literal[True]): If `value` is a valid IPv4 address. 83 | (ValidationError): If `value` is an invalid IPv4 address. 84 | """ 85 | if not value: 86 | return False 87 | try: 88 | if cidr: 89 | if strict and value.count("/") != 1: 90 | raise ValueError("IPv4 address was expected in CIDR notation") 91 | return IPv4Network(value, strict=not host_bit) and _check_private_ip(value, private) 92 | return IPv4Address(value) and _check_private_ip(value, private) 93 | except (ValueError, AddressValueError, NetmaskValueError): 94 | return False 95 | 96 | 97 | @validator 98 | def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bool = True): 99 | """Returns if a given value is a valid IPv6 address. 100 | 101 | Including IPv4-mapped IPv6 addresses. The initial version of ipv6 validator 102 | was inspired from [WTForms IPAddress validator][1]. 103 | 104 | [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py 105 | 106 | Examples: 107 | >>> ipv6('::ffff:192.0.2.128') 108 | True 109 | >>> ipv6('::1/128') 110 | True 111 | >>> ipv6('abc.0.0.1') 112 | ValidationError(func=ipv6, args={'value': 'abc.0.0.1'}) 113 | 114 | Args: 115 | value: 116 | IP address string to validate. 117 | cidr: 118 | IP address string may contain CIDR annotation. 119 | strict: 120 | IP address string is strictly in CIDR notation. 121 | host_bit: 122 | If `False` and host bits (along with network bits) _are_ set in the supplied 123 | address, this function raises a validation error. ref [IPv6Network][2]. 124 | [2]: https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv6Network 125 | 126 | Returns: 127 | (Literal[True]): If `value` is a valid IPv6 address. 128 | (ValidationError): If `value` is an invalid IPv6 address. 129 | """ 130 | if not value: 131 | return False 132 | try: 133 | if cidr: 134 | if strict and value.count("/") != 1: 135 | raise ValueError("IPv6 address was expected in CIDR notation") 136 | return IPv6Network(value, strict=not host_bit) 137 | return IPv6Address(value) 138 | except (ValueError, AddressValueError, NetmaskValueError): 139 | return False 140 | -------------------------------------------------------------------------------- /src/validators/length.py: -------------------------------------------------------------------------------- 1 | """Length.""" 2 | 3 | # standard 4 | from typing import Union 5 | 6 | # local 7 | from .between import between 8 | from .utils import validator 9 | 10 | 11 | @validator 12 | def length(value: str, /, *, min_val: Union[int, None] = None, max_val: Union[int, None] = None): 13 | """Return whether or not the length of given string is within a specified range. 14 | 15 | Examples: 16 | >>> length('something', min_val=2) 17 | True 18 | >>> length('something', min_val=9, max_val=9) 19 | True 20 | >>> length('something', max_val=5) 21 | ValidationError(func=length, args={'value': 'something', 'max_val': 5}) 22 | 23 | Args: 24 | value: 25 | The string to validate. 26 | min_val: 27 | The minimum required length of the string. If not provided, 28 | minimum length will not be checked. 29 | max_val: 30 | The maximum length of the string. If not provided, 31 | maximum length will not be checked. 32 | 33 | Returns: 34 | (Literal[True]): If `len(value)` is in between the given conditions. 35 | (ValidationError): If `len(value)` is not in between the given conditions. 36 | 37 | Raises: 38 | (ValueError): If either `min_val` or `max_val` is negative. 39 | """ 40 | if min_val is not None and min_val < 0: 41 | raise ValueError("Length cannot be negative. `min_val` is less than zero.") 42 | if max_val is not None and max_val < 0: 43 | raise ValueError("Length cannot be negative. `max_val` is less than zero.") 44 | 45 | return bool(between(len(value), min_val=min_val, max_val=max_val)) 46 | -------------------------------------------------------------------------------- /src/validators/mac_address.py: -------------------------------------------------------------------------------- 1 | """MAC Address.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | @validator 11 | def mac_address(value: str, /): 12 | """Return whether or not given value is a valid MAC address. 13 | 14 | This validator is based on [WTForms MacAddress validator][1]. 15 | 16 | [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py#L482 17 | 18 | Examples: 19 | >>> mac_address('01:23:45:67:ab:CD') 20 | True 21 | >>> mac_address('00:00:00:00:00') 22 | ValidationError(func=mac_address, args={'value': '00:00:00:00:00'}) 23 | 24 | Args: 25 | value: 26 | MAC address string to validate. 27 | 28 | Returns: 29 | (Literal[True]): If `value` is a valid MAC address. 30 | (ValidationError): If `value` is an invalid MAC address. 31 | """ 32 | return re.match(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", value) if value else False 33 | -------------------------------------------------------------------------------- /src/validators/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-validators/validators/e6d50ba532201ee9327b52ca084dcff9f60503d1/src/validators/py.typed -------------------------------------------------------------------------------- /src/validators/slug.py: -------------------------------------------------------------------------------- 1 | """Slug.""" 2 | 3 | # standard 4 | import re 5 | 6 | # local 7 | from .utils import validator 8 | 9 | 10 | @validator 11 | def slug(value: str, /): 12 | """Validate whether or not given value is valid slug. 13 | 14 | Valid slug can contain only lowercase alphanumeric characters and hyphens. 15 | It starts and ends with these lowercase alphanumeric characters. 16 | 17 | Examples: 18 | >>> slug('my-slug-2134') 19 | True 20 | >>> slug('my.slug') 21 | ValidationError(func=slug, args={'value': 'my.slug'}) 22 | 23 | Args: 24 | value: Slug string to validate. 25 | 26 | Returns: 27 | (Literal[True]): If `value` is a valid slug. 28 | (ValidationError): If `value` is an invalid slug. 29 | """ 30 | return re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", value) if value else False 31 | -------------------------------------------------------------------------------- /src/validators/uri.py: -------------------------------------------------------------------------------- 1 | """URI.""" 2 | 3 | # Read: https://stackoverflow.com/questions/176264 4 | # https://www.rfc-editor.org/rfc/rfc3986#section-3 5 | 6 | # local 7 | from .email import email 8 | from .url import url 9 | from .utils import validator 10 | 11 | 12 | def _file_url(value: str): 13 | if not value.startswith("file:///"): 14 | return False 15 | return True 16 | 17 | 18 | def _ipfs_url(value: str): 19 | if not value.startswith("ipfs://"): 20 | return False 21 | return True 22 | 23 | 24 | @validator 25 | def uri(value: str, /): 26 | """Return whether or not given value is a valid URI. 27 | 28 | Examples: 29 | >>> uri('mailto:example@domain.com') 30 | True 31 | >>> uri('file:path.txt') 32 | ValidationError(func=uri, args={'value': 'file:path.txt'}) 33 | 34 | Args: 35 | value: 36 | URI to validate. 37 | 38 | Returns: 39 | (Literal[True]): If `value` is a valid URI. 40 | (ValidationError): If `value` is an invalid URI. 41 | """ 42 | if not value: 43 | return False 44 | 45 | # TODO: work on various validations 46 | 47 | # url 48 | if any( 49 | # fmt: off 50 | value.startswith(item) 51 | for item in { 52 | "ftp", 53 | "ftps", 54 | "git", 55 | "http", 56 | "https", 57 | "irc", 58 | "rtmp", 59 | "rtmps", 60 | "rtsp", 61 | "sftp", 62 | "ssh", 63 | "telnet", 64 | } 65 | # fmt: on 66 | ): 67 | return url(value) 68 | 69 | # email 70 | if value.startswith("mailto:"): 71 | return email(value[len("mailto:") :]) 72 | 73 | # file 74 | if value.startswith("file:"): 75 | return _file_url(value) 76 | 77 | # ipfs 78 | if value.startswith("ipfs:"): 79 | return _ipfs_url(value) 80 | 81 | # magnet 82 | if value.startswith("magnet:?"): 83 | return True 84 | 85 | # telephone 86 | if value.startswith("tel:"): 87 | return True 88 | 89 | # data 90 | if value.startswith("data:"): 91 | return True 92 | 93 | # urn 94 | if value.startswith("urn:"): 95 | return True 96 | 97 | # urc 98 | if value.startswith("urc:"): 99 | return True 100 | 101 | return False 102 | -------------------------------------------------------------------------------- /src/validators/utils.py: -------------------------------------------------------------------------------- 1 | """Utils.""" 2 | 3 | # standard 4 | from functools import wraps 5 | from inspect import getfullargspec 6 | from itertools import chain 7 | from os import environ 8 | from typing import Any, Callable, Dict 9 | 10 | 11 | class ValidationError(Exception): 12 | """Exception class when validation failure occurs.""" 13 | 14 | def __init__(self, function: Callable[..., Any], arg_dict: Dict[str, Any], message: str = ""): 15 | """Initialize Validation Failure.""" 16 | if message: 17 | self.reason = message 18 | self.func = function 19 | self.__dict__.update(arg_dict) 20 | 21 | def __repr__(self): 22 | """Repr Validation Failure.""" 23 | return ( 24 | f"ValidationError(func={self.func.__name__}, " 25 | + f"args={ ({k: v for (k, v) in self.__dict__.items() if k != 'func'}) })" 26 | ) 27 | 28 | def __str__(self): 29 | """Str Validation Failure.""" 30 | return repr(self) 31 | 32 | def __bool__(self): 33 | """Bool Validation Failure.""" 34 | return False 35 | 36 | 37 | def _func_args_as_dict(func: Callable[..., Any], *args: Any, **kwargs: Any): 38 | """Return function's positional and key value arguments as an ordered dictionary.""" 39 | return dict( 40 | list(zip(dict.fromkeys(chain(getfullargspec(func)[0], kwargs.keys())), args)) 41 | + list(kwargs.items()) 42 | ) 43 | 44 | 45 | def validator(func: Callable[..., Any]): 46 | """A decorator that makes given function validator. 47 | 48 | Whenever the given `func` returns `False` this 49 | decorator returns `ValidationError` object. 50 | 51 | Examples: 52 | >>> @validator 53 | ... def even(value): 54 | ... return not (value % 2) 55 | >>> even(4) 56 | True 57 | >>> even(5) 58 | ValidationError(func=even, args={'value': 5}) 59 | 60 | Args: 61 | func: 62 | Function which is to be decorated. 63 | 64 | Returns: 65 | (Callable[..., ValidationError | Literal[True]]): 66 | A decorator which returns either `ValidationError` 67 | or `Literal[True]`. 68 | 69 | Raises: 70 | (ValidationError): If `r_ve` or `RAISE_VALIDATION_ERROR` is `True` 71 | """ 72 | 73 | @wraps(func) 74 | def wrapper(*args: Any, **kwargs: Any): 75 | raise_validation_error = False 76 | if "r_ve" in kwargs: 77 | raise_validation_error = True 78 | del kwargs["r_ve"] 79 | if environ.get("RAISE_VALIDATION_ERROR", "False") == "True": 80 | raise_validation_error = True 81 | 82 | try: 83 | if raise_validation_error: 84 | if func(*args, **kwargs): 85 | return True 86 | else: 87 | raise ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 88 | else: 89 | return ( 90 | True 91 | if func(*args, **kwargs) 92 | else ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) 93 | ) 94 | except (ValueError, TypeError, UnicodeError) as exp: 95 | if raise_validation_error: 96 | raise ValidationError( 97 | func, _func_args_as_dict(func, *args, **kwargs), str(exp) 98 | ) from exp 99 | else: 100 | return ValidationError(func, _func_args_as_dict(func, *args, **kwargs), str(exp)) 101 | 102 | return wrapper 103 | -------------------------------------------------------------------------------- /src/validators/uuid.py: -------------------------------------------------------------------------------- 1 | """UUID.""" 2 | 3 | # standard 4 | import re 5 | from typing import Union 6 | from uuid import UUID 7 | 8 | # local 9 | from .utils import validator 10 | 11 | 12 | @validator 13 | def uuid(value: Union[str, UUID], /): 14 | """Return whether or not given value is a valid UUID-v4 string. 15 | 16 | This validator is based on [WTForms UUID validator][1]. 17 | 18 | [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py#L539 19 | 20 | Examples: 21 | >>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8') 22 | True 23 | >>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8') 24 | ValidationError(func=uuid, args={'value': '2bc1c94f 0deb-43e9-92a1-4775189ec9f8'}) 25 | 26 | Args: 27 | value: 28 | UUID string or object to validate. 29 | 30 | Returns: 31 | (Literal[True]): If `value` is a valid UUID. 32 | (ValidationError): If `value` is an invalid UUID. 33 | """ 34 | if not value: 35 | return False 36 | if isinstance(value, UUID): 37 | return True 38 | try: 39 | return UUID(value) or re.match( 40 | r"^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$", value 41 | ) 42 | except ValueError: 43 | return False 44 | -------------------------------------------------------------------------------- /tests/crypto_addresses/test_bsc_address.py: -------------------------------------------------------------------------------- 1 | """Test BSC address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, bsc_address 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | "0x4e5acf9684652BEa56F2f01b7101a225Ee33d23f", 14 | "0x22B0f92af10FdC25659e4C3A590c2F0D0c809c27", 15 | "0xb61724F993E7942ef2d8e4A94fF7c9e1cc26995F", 16 | "0x9c3dF8a511Fec8076D4B8EFb4d5E733B9F953dD7", 17 | "0x4536337B91c0623a4FD098023E6065e4773117c5", 18 | "0xAC484e1CE274eD1d40A7C2AeAb0bEA863634286F", 19 | "0x1FDE521fBe3483Cbb5957E6275028225a74387e4", 20 | "0x1693c3D1bA787Ba2bf81ac8897614AAaee5cb800", 21 | "0xf4C3Fd476A40658aEd9e595DA49c37d8965D2fFE", 22 | "0xc053E3D4932640787D6Cf67FcA36021E7BE62653", 23 | "0xaFd563A5aED0bC363e802842aD93Af46c1168b8a", 24 | ], 25 | ) 26 | def test_returns_true_on_valid_bsc_address(value: str): 27 | """Test returns true on valid bsc address.""" 28 | assert bsc_address(value) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "value", 33 | [ 34 | "1x32Be343B94f860124dC4fEe278FDCBD38C102D88", 35 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D", 36 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D88aabbcc", 37 | "0x4g5acf9684652BEa56F2f01b7101a225Eh33d23z", 38 | "0x", 39 | "Wrong@Address.com", 40 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D__", 41 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D88G", 42 | "0X32Be343B94f860124dC4fEe278FDCBD38C102D88", 43 | "0X32BE343B94F860124DCFEE278FDCBD38C102D88", 44 | "0x32Be 343B94f860124dC4fEe278FDCBD38C102D88", 45 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D88!", 46 | "ox32Be343B94f860124dC4fEe278FDCBD38C102D88", 47 | "0x32Be343B94f860124dC4fEe278FDCBD38C102D88XYZ", 48 | ], 49 | ) 50 | def test_returns_failed_validation_on_invalid_bsc_address(value: str): 51 | """Test returns failed validation on invalid bsc address.""" 52 | assert isinstance(bsc_address(value), ValidationError) 53 | -------------------------------------------------------------------------------- /tests/crypto_addresses/test_btc_address.py: -------------------------------------------------------------------------------- 1 | """Test BTC address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, btc_address 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | # P2PKH (Pay-to-PubkeyHash) type 14 | "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", 15 | # P2SH (Pay to script hash) type 16 | "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", 17 | # Bech32/segwit type 18 | "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", 19 | "bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9", 20 | ], 21 | ) 22 | def test_returns_true_on_valid_btc_address(value: str): 23 | """Test returns true on valid btc address.""" 24 | assert btc_address(value) 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "value", 29 | [ 30 | "ff3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69", 31 | "b3Cgwgr2g7vsi1bXyjyDUkphEnVoRLA9w4FZfC69", 32 | # incorrect header 33 | "1BvBMsEYstWetqTFn5Au4m4GFg7xJaNVN2", 34 | # incorrect checksum 35 | "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz", 36 | ], 37 | ) 38 | def test_returns_failed_validation_on_invalid_btc_address(value: str): 39 | """Test returns failed validation on invalid btc address.""" 40 | assert isinstance(btc_address(value), ValidationError) 41 | -------------------------------------------------------------------------------- /tests/crypto_addresses/test_eth_address.py: -------------------------------------------------------------------------------- 1 | """Test ETH address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, eth_address 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | "0x8ba1f109551bd432803012645ac136ddd64dba72", 14 | "0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598", 15 | "0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE", 16 | "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 17 | "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", 18 | "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 19 | "0x1234567890123456789012345678901234567890", 20 | "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", 21 | ], 22 | ) 23 | def test_returns_true_on_valid_eth_address(value: str): 24 | """Test returns true on valid eth address.""" 25 | assert eth_address(value) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "value", 30 | [ 31 | "0x742d35Cc6634C0532925a3b844Bc454e4438f44g", 32 | "0x742d35Cc6634C0532925a3b844Bc454e4438f44", 33 | "0xAbcdefg1234567890Abcdefg1234567890Abcdefg", 34 | "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c72", 35 | "0x80fBD7F8B3f81D0e1d6EACAb69AF104A6508AFB1", 36 | "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c7g", 37 | "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c", 38 | "0x7Fb21a171205f3B8d8E4d88A2d2f8A56E45DdB5c", 39 | "validators.eth", 40 | ], 41 | ) 42 | def test_returns_failed_validation_on_invalid_eth_address(value: str): 43 | """Test returns failed validation on invalid eth address.""" 44 | assert isinstance(eth_address(value), ValidationError) 45 | -------------------------------------------------------------------------------- /tests/crypto_addresses/test_trx_address.py: -------------------------------------------------------------------------------- 1 | """Test TRX address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, trx_address 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | "TLjfbTbpZYDQ4EoA4N5CLNgGjfbF8ZWz38", 14 | "TDQ6C92wuNqvMWE967sMptCFaXq77uj1PF", 15 | "TFuGbxCQGSL4oLnJzVsen844LDwFbrUY4e", 16 | "TFAPKADDRhkSe3v27CsR8TZSjN8eJ8ycDK", 17 | "TSJHywLNva2MNjCD5iYfn5QAKD9Rk5Ncit", 18 | "TEi1qhi5LuTicg1u9oAstyXCSf5uibSyqo", 19 | "TAGvx5An6VBeHTu91cQwdABNcAYMRPcP4n", 20 | "TXbE5tXTejqT3Q47sYKCDb9NJDm3xrFpab", 21 | "TMTxQWNuWHXvHcYXc5D1wQhFmZFJijAxcG", 22 | "TPHgw9E8QYM3esNWih5KVnUVpUHwLTPfpA", 23 | "TFFLtBTi9jdaGwV3hznjCmPYaJme5AeqwU", 24 | "TC74QG8tbtixG5Raa4fEifywgjrFs45fNz", 25 | ], 26 | ) 27 | def test_returns_true_on_valid_trx_address(value: str): 28 | """Test returns true on valid trx address.""" 29 | assert trx_address(value) 30 | 31 | 32 | @pytest.mark.parametrize( 33 | "value", 34 | [ 35 | "T12345678901234567890123456789012345", 36 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678", 37 | "TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vd", 38 | "TP6ah2v5mdsj8Z3hGz1yDMvDq7BzEbK8o", 39 | "TQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4XX", 40 | "TQNy2C6VHJPk4P32bsEX3QSGx2Qqm4J2k9", 41 | "TP6ah2v5mdsj8Z3hGz1yDMvDq7BzEbK8oN", 42 | "TSTVdfU1x4L7K3Bc3v5C28Gp2J1rPyeL3f", 43 | "THPByuCzvU5QER9j2NC2mUQ2JPyRCam4e7", 44 | "TW5eZqUZgdW4rxFKAKsc2ryJbfFA94WXvD", 45 | "TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vdd", 46 | "tQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4X", 47 | "TR2G7Rm4vFqF8EpY4U5xdLdQ7Xg", 48 | "TQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4x", 49 | "my-trox-address.trx", 50 | ], 51 | ) 52 | def test_returns_failed_validation_on_invalid_trx_address(value: str): 53 | """Test returns failed validation on invalid trx address.""" 54 | assert isinstance(trx_address(value), ValidationError) 55 | -------------------------------------------------------------------------------- /tests/i18n/test_es.py: -------------------------------------------------------------------------------- 1 | """Test i18n/es.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, es_cif, es_doi, es_nie, es_nif 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value",), 12 | [ 13 | ("B25162520",), 14 | ("U4839822F",), 15 | ("B96817697",), 16 | ("P7067074J",), 17 | ("Q7899705C",), 18 | ("C75098681",), 19 | ("G76061860",), 20 | ("C71345375",), 21 | ("G20558169",), 22 | ("U5021960I",), 23 | ], 24 | ) 25 | def test_returns_true_on_valid_cif(value: str): 26 | """Test returns true on valid cif.""" 27 | assert es_cif(value) 28 | 29 | 30 | @pytest.mark.parametrize( 31 | ("value",), 32 | [ 33 | ("12345",), 34 | ("ABCDEFGHI",), 35 | ("Z5021960I",), 36 | ], 37 | ) 38 | def test_returns_false_on_invalid_cif(value: str): 39 | """Test returns false on invalid cif.""" 40 | result = es_cif(value) 41 | assert isinstance(result, ValidationError) 42 | 43 | 44 | @pytest.mark.parametrize( 45 | ("value",), 46 | [ 47 | ("X0095892M",), 48 | ("X8868108K",), 49 | ("X2911154K",), 50 | ("Y2584969J",), 51 | ("X7536157T",), 52 | ("Y5840388N",), 53 | ("Z2915723H",), 54 | ("Y4002236C",), 55 | ("X7750702R",), 56 | ("Y0408759V",), 57 | ], 58 | ) 59 | def test_returns_true_on_valid_nie(value: str): 60 | """Test returns true on valid nie.""" 61 | assert es_nie(value) 62 | 63 | 64 | @pytest.mark.parametrize( 65 | ("value",), 66 | [ 67 | ("K0000023T",), 68 | ("L0000024R",), 69 | ("M0000025W",), 70 | ("00000026A",), 71 | ("00000027G",), 72 | ("00000028M",), 73 | ("00000029Y",), 74 | ("00000030F",), 75 | ("00000031P",), 76 | ("00000032D",), 77 | ("00000033X",), 78 | ("00000034B",), 79 | ("00000035N",), 80 | ("00000036J",), 81 | ("00000037Z",), 82 | ("00000038S",), 83 | ("00000039Q",), 84 | ("00000040V",), 85 | ("00000041H",), 86 | ("00000042L",), 87 | ("00000043C",), 88 | ("00000044K",), 89 | ("00000045E",), 90 | ], 91 | ) 92 | def test_returns_true_on_valid_nif(value: str): 93 | """Test returns true on valid nif.""" 94 | assert es_nif(value) 95 | 96 | 97 | def test_returns_false_on_invalid_nif(): 98 | """Test returns false on invalid nif.""" 99 | result = es_nif("12345") 100 | assert isinstance(result, ValidationError) 101 | 102 | 103 | @pytest.mark.parametrize( 104 | ("value",), 105 | [ 106 | # CIFs 107 | ("B25162520",), 108 | ("U4839822F",), 109 | ("B96817697",), 110 | # NIEs 111 | ("X0000000T",), 112 | ("X0095892M",), 113 | ("X8868108K",), 114 | ("X2911154K",), 115 | # NIFs 116 | ("00000001R",), 117 | ("00000000T",), 118 | ("26643189N",), 119 | ("07060225F",), 120 | ("49166693F",), 121 | ], 122 | ) 123 | def test_returns_true_on_valid_doi(value: str): 124 | """Test returns true on valid doi.""" 125 | assert es_doi(value) 126 | -------------------------------------------------------------------------------- /tests/i18n/test_fi.py: -------------------------------------------------------------------------------- 1 | """Test i18n/es.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError 8 | from validators.i18n.fi import fi_business_id, fi_ssn 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("value",), 13 | [ 14 | ("2336509-6",), # Supercell 15 | ("0112038-9",), # Fast Monkeys 16 | ("2417581-7",), # Nokia 17 | ], 18 | ) 19 | def test_returns_true_on_valid_business_id(value: str): 20 | """Test returns true on valid business id.""" 21 | assert fi_business_id(value) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ("value",), 26 | [ 27 | (None,), 28 | ("",), 29 | ("1233312312",), 30 | ("1333333-8",), 31 | ("1231233-9",), 32 | ], 33 | ) 34 | def test_returns_failed_validation_on_invalid_business_id(value: str): 35 | """Test returns failed validation on invalid business id.""" 36 | assert isinstance(fi_business_id(value), ValidationError) 37 | 38 | 39 | @pytest.mark.parametrize( 40 | ("value",), 41 | [ 42 | ("010190-002R",), 43 | ("010101-0101",), 44 | ("010101+0101",), 45 | ("010101A0101",), 46 | ("010190-900P",), 47 | ("020516C903K",), 48 | ("010594Y9032",), 49 | ], 50 | ) 51 | def test_returns_true_on_valid_ssn(value: str): 52 | """Test returns true on valid ssn.""" 53 | assert fi_ssn(value) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | ("value",), 58 | [ 59 | (None,), 60 | ("",), 61 | ("010190-001P",), # Too low serial 62 | ("010190-000N",), # Too low serial 63 | ("000190-0023",), # Invalid day 64 | ("010090-002X",), # Invalid month 65 | ("010190-002r",), # Invalid checksum 66 | ("101010-0102",), 67 | ("10a010-0101",), 68 | ("101010-0\xe401",), 69 | ("101010b0101",), 70 | ("0205169C03K",), 71 | ("0105949Y032",), 72 | ], 73 | ) 74 | def test_returns_failed_validation_on_invalid_ssn(value: str): 75 | """Test returns failed validation on invalid_ssn.""" 76 | assert isinstance(fi_ssn(value), ValidationError) 77 | 78 | 79 | def test_returns_failed_validation_on_temporal_ssn_when_not_allowed(): 80 | """Test returns failed validation on temporal-ssn when not allowed.""" 81 | assert isinstance(fi_ssn("010190-900P", allow_temporal_ssn=False), ValidationError) 82 | -------------------------------------------------------------------------------- /tests/i18n/test_fr.py: -------------------------------------------------------------------------------- 1 | """Test French validators.""" 2 | 3 | # standard 4 | from typing import Union 5 | 6 | # external 7 | import pytest 8 | 9 | # local 10 | from validators import ValidationError 11 | from validators.i18n.fr import fr_department, fr_ssn 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("value",), 16 | [ 17 | ("1 84 12 76 451 089 46",), 18 | ("1 84 12 76 451 089",), # control key is optional 19 | ("2 99 05 75 202 818 97",), 20 | ("2 99 05 75 202 817 01",), 21 | ("2 99 05 2A 202 817 58",), 22 | ("2 99 05 2B 202 817 85",), 23 | ("2 99 05 971 12 817 70",), 24 | ], 25 | ) 26 | def test_returns_true_on_valid_ssn(value: str): 27 | """Test returns true on valid ssn.""" 28 | assert fr_ssn(value) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ("value",), 33 | [ 34 | (None,), 35 | ("",), 36 | ("3 84 12 76 451 089 46",), # wrong gender number 37 | ("1 84 12 76 451 089 47",), # wrong control key 38 | ("1 84 00 76 451 089",), # invalid month 39 | ("1 84 13 76 451 089",), # invalid month 40 | ("1 84 12 00 451 089",), # invalid department 41 | ("1 84 12 2C 451 089",), 42 | ("1 84 12 98 451 089",), # invalid department 43 | # ("1 84 12 971 451 089",), # ? 44 | ], 45 | ) 46 | def test_returns_failed_validation_on_invalid_ssn(value: str): 47 | """Test returns failed validation on invalid_ssn.""" 48 | assert isinstance(fr_ssn(value), ValidationError) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | ("value",), 53 | [ 54 | ("01",), 55 | ("2A",), # Corsica 56 | ("2B",), 57 | (14,), 58 | ("95",), 59 | ("971",), 60 | (971,), 61 | ], 62 | ) 63 | def test_returns_true_on_valid_department(value: Union[str, int]): 64 | """Test returns true on valid department.""" 65 | assert fr_department(value) 66 | 67 | 68 | @pytest.mark.parametrize( 69 | ("value",), 70 | [ 71 | (None,), 72 | ("",), 73 | ("00",), 74 | (0,), 75 | ("2C",), 76 | ("97",), 77 | ("978",), 78 | ("98",), 79 | ("96",), 80 | ("20",), 81 | (20,), 82 | ], 83 | ) 84 | def test_returns_failed_validation_on_invalid_department(value: Union[str, int]): 85 | """Test returns failed validation on invalid department.""" 86 | assert isinstance(fr_department(value), ValidationError) 87 | -------------------------------------------------------------------------------- /tests/i18n/test_ind.py: -------------------------------------------------------------------------------- 1 | """Test Indian validators.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError 8 | from validators.i18n import ind_aadhar, ind_pan 9 | 10 | 11 | @pytest.mark.parametrize("value", ["3675 9834 6012", "5046 3182 4299"]) 12 | def test_returns_true_on_valid_ind_aadhar(value: str): 13 | """Test returns true on valid ind aadhar.""" 14 | assert ind_aadhar(value) 15 | 16 | 17 | @pytest.mark.parametrize("value", ["3675 9834 6012 8", "417598346012", "3675 98AF 60#2"]) 18 | def test_returns_failed_validation_on_invalid_ind_aadhar(value: str): 19 | """Test returns failed validation on invalid ind aadhar.""" 20 | assert isinstance(ind_aadhar(value), ValidationError) 21 | 22 | 23 | @pytest.mark.parametrize("value", ["ABCDE9999K", "AAAPL1234C"]) 24 | def test_returns_true_on_valid_ind_pan(value: str): 25 | """Test returns true on valid ind pan.""" 26 | assert ind_pan(value) 27 | 28 | 29 | @pytest.mark.parametrize("value", ["ABC5d7896B", "417598346012", "AaaPL1234C"]) 30 | def test_returns_failed_validation_on_invalid_ind_pan(value: str): 31 | """Test returns failed validation on invalid ind pan.""" 32 | assert isinstance(ind_pan(value), ValidationError) 33 | -------------------------------------------------------------------------------- /tests/i18n/test_ru.py: -------------------------------------------------------------------------------- 1 | """Test i18n/inn.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError 8 | from validators.i18n.ru import ru_inn 9 | 10 | 11 | @pytest.mark.parametrize( 12 | ("value",), 13 | [ 14 | ("2222058686",), 15 | ("7709439560",), 16 | ("5003052454",), 17 | ("7730257499",), 18 | ("3664016814",), 19 | ("026504247480",), 20 | ("780103209220",), 21 | ("7707012148",), 22 | ("140700989885",), 23 | ("774334078053",), 24 | ], 25 | ) 26 | def test_returns_true_on_valid_ru_inn(value: str): 27 | """Test returns true on valid russian individual tax number.""" 28 | assert ru_inn(value) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ("value",), 33 | [ 34 | ("2222058687",), 35 | ("7709439561",), 36 | ("5003052453",), 37 | ("7730257490",), 38 | ("3664016815",), 39 | ("026504247481",), 40 | ("780103209222",), 41 | ("7707012149",), 42 | ("140700989886",), 43 | ("774334078054",), 44 | ], 45 | ) 46 | def test_returns_false_on_valid_ru_inn(value: str): 47 | """Test returns true on valid russian individual tax number.""" 48 | assert isinstance(ru_inn(value), ValidationError) 49 | -------------------------------------------------------------------------------- /tests/test__extremes.py: -------------------------------------------------------------------------------- 1 | """Test Extremes.""" 2 | 3 | # standard 4 | from typing import Any 5 | 6 | # external 7 | import pytest 8 | 9 | # local 10 | from validators._extremes import AbsMax, AbsMin 11 | 12 | abs_max = AbsMax() 13 | abs_min = AbsMin() 14 | 15 | 16 | @pytest.mark.parametrize( 17 | ("value",), 18 | [(None,), ("",), (12,), (abs_min,)], 19 | ) 20 | def test_abs_max_is_greater_than_every_other_value(value: Any): 21 | """Test if AbsMax is greater than every other value.""" 22 | assert value < abs_max 23 | assert abs_max > value 24 | 25 | 26 | def test_abs_max_is_not_greater_than_itself(): 27 | """Test if AbsMax is not greater than itself.""" 28 | assert not (abs_max > abs_max) 29 | 30 | 31 | def test_other_comparison_methods_for_abs_max(): 32 | """Test other comparison methods for AbsMax.""" 33 | assert abs_max <= abs_max 34 | assert abs_max == abs_max 35 | assert abs_max == abs_max 36 | 37 | 38 | @pytest.mark.parametrize( 39 | ("value",), 40 | [(None,), ("",), (12,), (abs_max,)], 41 | ) 42 | def test_abs_min_is_smaller_than_every_other_value(value: Any): 43 | """Test if AbsMin is less than every other value.""" 44 | assert value > abs_min 45 | 46 | 47 | def test_abs_min_is_not_greater_than_itself(): 48 | """Test if AbsMin is not less than itself.""" 49 | assert not (abs_min < abs_min) 50 | 51 | 52 | def test_other_comparison_methods_for_abs_min(): 53 | """Test other comparison methods for AbsMin.""" 54 | assert abs_min <= abs_min 55 | assert abs_min == abs_min 56 | assert abs_min == abs_min 57 | -------------------------------------------------------------------------------- /tests/test_between.py: -------------------------------------------------------------------------------- 1 | """Test Between.""" 2 | 3 | # standard 4 | from datetime import datetime 5 | from typing import TypeVar 6 | 7 | # external 8 | import pytest 9 | 10 | # local 11 | from validators import ValidationError, between 12 | 13 | T = TypeVar("T", int, float, str, datetime) 14 | 15 | 16 | @pytest.mark.parametrize( 17 | ("value", "min_val", "max_val"), 18 | [(12, 11, 13), (12, None, 14), (12, 11, None), (12, 12, 12), (0, 0, 0), (0, -1, 3)], 19 | ) 20 | def test_returns_true_on_valid_range(value: T, min_val: T, max_val: T): 21 | """Test returns true on valid range.""" 22 | assert between(value, min_val=min_val, max_val=max_val) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | ("value", "min_val", "max_val"), 27 | [ 28 | (None, 13, 14), 29 | (12, 13, 14), 30 | (12, None, 11), 31 | (12, 13, None), 32 | (12, "13.5", datetime(1970, 1, 1)), 33 | ("12", 20.5, "None"), 34 | (datetime(1970, 1, 1), 20, "string"), 35 | (30, 40, "string"), 36 | ], 37 | ) 38 | def test_returns_failed_validation_on_invalid_range(value: T, min_val: T, max_val: T): 39 | """Test returns failed validation on invalid range.""" 40 | assert isinstance(between(value, min_val=min_val, max_val=max_val), ValidationError) 41 | -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | """Test Card.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ( 8 | ValidationError, 9 | amex, 10 | card_number, 11 | diners, 12 | discover, 13 | jcb, 14 | mastercard, 15 | mir, 16 | unionpay, 17 | visa, 18 | ) 19 | 20 | visa_cards = ["4242424242424242", "4000002760003184"] 21 | mastercard_cards = ["5555555555554444", "2223003122003222"] 22 | amex_cards = ["378282246310005", "371449635398431"] 23 | unionpay_cards = ["6200000000000005"] 24 | diners_cards = ["3056930009020004", "36227206271667"] 25 | jcb_cards = ["3566002020360505"] 26 | discover_cards = ["6011111111111117", "6011000990139424"] 27 | mir_cards = ["2200123456789019", "2204987654321098"] 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "value", 32 | visa_cards 33 | + mastercard_cards 34 | + amex_cards 35 | + unionpay_cards 36 | + diners_cards 37 | + jcb_cards 38 | + discover_cards 39 | + mir_cards, 40 | ) 41 | def test_returns_true_on_valid_card_number(value: str): 42 | """Test returns true on valid card number.""" 43 | assert card_number(value) 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "value", 48 | [ 49 | "4242424242424240", 50 | "4000002760003180", 51 | "400000276000318X", 52 | "220012345678901X", 53 | ], 54 | ) 55 | def test_returns_failed_on_valid_card_number(value: str): 56 | """Test returns failed on valid card number.""" 57 | assert isinstance(card_number(value), ValidationError) 58 | 59 | 60 | @pytest.mark.parametrize("value", visa_cards) 61 | def test_returns_true_on_valid_visa(value: str): 62 | """Test returns true on valid visa.""" 63 | assert visa(value) 64 | 65 | 66 | @pytest.mark.parametrize( 67 | "value", 68 | mastercard_cards + amex_cards + unionpay_cards + diners_cards + jcb_cards + discover_cards, 69 | ) 70 | def test_returns_failed_on_valid_visa(value: str): 71 | """Test returns failed on valid visa.""" 72 | assert isinstance(visa(value), ValidationError) 73 | 74 | 75 | @pytest.mark.parametrize("value", mastercard_cards) 76 | def test_returns_true_on_valid_mastercard(value: str): 77 | """Test returns true on valid mastercard.""" 78 | assert mastercard(value) 79 | 80 | 81 | @pytest.mark.parametrize( 82 | "value", 83 | visa_cards + amex_cards + unionpay_cards + diners_cards + jcb_cards + discover_cards, 84 | ) 85 | def test_returns_failed_on_valid_mastercard(value: str): 86 | """Test returns failed on valid mastercard.""" 87 | assert isinstance(mastercard(value), ValidationError) 88 | 89 | 90 | @pytest.mark.parametrize("value", amex_cards) 91 | def test_returns_true_on_valid_amex(value: str): 92 | """Test returns true on valid amex.""" 93 | assert amex(value) 94 | 95 | 96 | @pytest.mark.parametrize( 97 | "value", 98 | visa_cards 99 | + mastercard_cards 100 | + unionpay_cards 101 | + diners_cards 102 | + jcb_cards 103 | + discover_cards 104 | + mir_cards, 105 | ) 106 | def test_returns_failed_on_valid_amex(value: str): 107 | """Test returns failed on valid amex.""" 108 | assert isinstance(amex(value), ValidationError) 109 | 110 | 111 | @pytest.mark.parametrize("value", unionpay_cards) 112 | def test_returns_true_on_valid_unionpay(value: str): 113 | """Test returns true on valid unionpay.""" 114 | assert unionpay(value) 115 | 116 | 117 | @pytest.mark.parametrize( 118 | "value", 119 | visa_cards 120 | + mastercard_cards 121 | + amex_cards 122 | + diners_cards 123 | + jcb_cards 124 | + discover_cards 125 | + mir_cards, 126 | ) 127 | def test_returns_failed_on_valid_unionpay(value: str): 128 | """Test returns failed on valid unionpay.""" 129 | assert isinstance(unionpay(value), ValidationError) 130 | 131 | 132 | @pytest.mark.parametrize("value", diners_cards) 133 | def test_returns_true_on_valid_diners(value: str): 134 | """Test returns true on valid diners.""" 135 | assert diners(value) 136 | 137 | 138 | @pytest.mark.parametrize( 139 | "value", 140 | visa_cards 141 | + mastercard_cards 142 | + amex_cards 143 | + unionpay_cards 144 | + jcb_cards 145 | + discover_cards 146 | + mir_cards, 147 | ) 148 | def test_returns_failed_on_valid_diners(value: str): 149 | """Test returns failed on valid diners.""" 150 | assert isinstance(diners(value), ValidationError) 151 | 152 | 153 | @pytest.mark.parametrize("value", jcb_cards) 154 | def test_returns_true_on_valid_jcb(value: str): 155 | """Test returns true on valid jcb.""" 156 | assert jcb(value) 157 | 158 | 159 | @pytest.mark.parametrize( 160 | "value", 161 | visa_cards 162 | + mastercard_cards 163 | + amex_cards 164 | + unionpay_cards 165 | + diners_cards 166 | + discover_cards 167 | + mir_cards, 168 | ) 169 | def test_returns_failed_on_valid_jcb(value: str): 170 | """Test returns failed on valid jcb.""" 171 | assert isinstance(jcb(value), ValidationError) 172 | 173 | 174 | @pytest.mark.parametrize("value", discover_cards) 175 | def test_returns_true_on_valid_discover(value: str): 176 | """Test returns true on valid discover.""" 177 | assert discover(value) 178 | 179 | 180 | @pytest.mark.parametrize( 181 | "value", 182 | visa_cards 183 | + mastercard_cards 184 | + amex_cards 185 | + unionpay_cards 186 | + diners_cards 187 | + jcb_cards 188 | + mir_cards, 189 | ) 190 | def test_returns_failed_on_valid_discover(value: str): 191 | """Test returns failed on valid discover.""" 192 | assert isinstance(discover(value), ValidationError) 193 | 194 | 195 | @pytest.mark.parametrize("value", mir_cards) 196 | def test_returns_true_on_valid_mir(value: str): 197 | """Test returns true on valid Mir card.""" 198 | assert mir(value) 199 | 200 | 201 | @pytest.mark.parametrize( 202 | "value", 203 | visa_cards 204 | + mastercard_cards 205 | + amex_cards 206 | + unionpay_cards 207 | + diners_cards 208 | + jcb_cards 209 | + discover_cards, 210 | ) 211 | def test_returns_failed_on_valid_mir(value: str): 212 | """Test returns failed on invalid Mir card (other payment systems).""" 213 | assert isinstance(mir(value), ValidationError) 214 | -------------------------------------------------------------------------------- /tests/test_country.py: -------------------------------------------------------------------------------- 1 | """Test Country.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, calling_code, country_code, currency 8 | 9 | 10 | @pytest.mark.parametrize(("value"), ["+1", "+371"]) 11 | def test_returns_true_on_valid_calling_code(value: str): 12 | """Test returns true on valid calling code.""" 13 | assert calling_code(value) 14 | 15 | 16 | @pytest.mark.parametrize(("value"), ["+19", "+37", "-9"]) 17 | def test_returns_failed_validation_invalid_calling_code(value: str): 18 | """Test returns failed validation invalid calling code.""" 19 | assert isinstance(calling_code(value), ValidationError) 20 | 21 | 22 | @pytest.mark.parametrize( 23 | ("value", "iso_format"), 24 | [ 25 | ("ISR", "auto"), 26 | ("US", "alpha2"), 27 | ("USA", "alpha3"), 28 | ("840", "numeric"), 29 | ], 30 | ) 31 | def test_returns_true_on_valid_country_code(value: str, iso_format: str): 32 | """Test returns true on valid country code.""" 33 | assert country_code(value, iso_format=iso_format) 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ("value", "iso_format"), 38 | [ 39 | (None, "auto"), 40 | ("", "auto"), 41 | ("123456", "auto"), 42 | ("XY", "alpha2"), 43 | ("PPP", "alpha3"), 44 | ("123", "numeric"), 45 | ("us", "auto"), 46 | ("uSa", "auto"), 47 | ("US ", "auto"), 48 | ("U.S", "auto"), 49 | ("1ND", "unknown"), 50 | ("ISR", None), 51 | ], 52 | ) 53 | def test_returns_failed_validation_on_invalid_country_code(value: str, iso_format: str): 54 | """Test returns failed validation on invalid country code.""" 55 | assert isinstance(country_code(value, iso_format=iso_format), ValidationError) 56 | 57 | 58 | @pytest.mark.parametrize( 59 | ("value", "skip_symbols", "ignore_case"), [("$", False, False), ("uSd", True, True)] 60 | ) 61 | def test_returns_true_on_valid_currency(value: str, skip_symbols: bool, ignore_case: bool): 62 | """Test returns true on valid currency.""" 63 | assert currency(value, skip_symbols=skip_symbols, ignore_case=ignore_case) 64 | 65 | 66 | @pytest.mark.parametrize( 67 | ("value", "skip_symbols", "ignore_case"), 68 | [("$", True, False), ("uSd", True, False), ("Bucks", True, True)], 69 | ) 70 | def test_returns_failed_validation_invalid_currency( 71 | value: str, skip_symbols: bool, ignore_case: bool 72 | ): 73 | """Test returns failed validation invalid currency.""" 74 | assert isinstance( 75 | currency(value, skip_symbols=skip_symbols, ignore_case=ignore_case), ValidationError 76 | ) 77 | -------------------------------------------------------------------------------- /tests/test_cron.py: -------------------------------------------------------------------------------- 1 | """Test Cron.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, cron 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | "* * * * *", 14 | "*/5 * * * *", 15 | "0 0 * * *", 16 | "30 3 * * 1-5", 17 | "15 5 * * 1,3,5", 18 | "0 12 1 */2 *", 19 | "0 */3 * * *", 20 | "0 0 1 1 *", 21 | "0 12 * 1-6 1-5", 22 | "0 3-6 * * *", 23 | "*/15 0,6,12,18 * * *", 24 | "0 12 * * 0", 25 | "*/61 * * * *", 26 | # "5-10/2 * * * *", # this is valid, but not supported yet 27 | ], 28 | ) 29 | def test_returns_true_on_valid_cron(value: str): 30 | """Test returns true on valid cron string.""" 31 | assert cron(value) 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "value", 36 | [ 37 | "* * * * * *", 38 | "* * * *", 39 | "*/5 25 * * *", 40 | "*/5 * *-1 * *", 41 | "32-30 * * * *", 42 | "0 12 32 * *", 43 | "0 12 * * 8", 44 | "0 */0 * * *", 45 | "30-20 * * * *", 46 | "10-* * * * *", 47 | "*/15 0,6,12,24 * * *", 48 | "& * * & * *", 49 | "* - * * - *", 50 | ], 51 | ) 52 | def test_returns_failed_validation_on_invalid_cron(value: str): 53 | """Test returns failed validation on invalid cron string.""" 54 | assert isinstance(cron(value), ValidationError) 55 | -------------------------------------------------------------------------------- /tests/test_domain.py: -------------------------------------------------------------------------------- 1 | """Test Domain.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, domain 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value", "rfc_1034", "rfc_2782"), 12 | [ 13 | ("example.com", False, False), 14 | ("exa_mple.com", False, True), 15 | ("xn----gtbspbbmkef.xn--p1ai", False, False), 16 | ("underscore_subdomain.example.com", False, True), 17 | ("something.versicherung", False, False), 18 | ("someThing.versicherung.", True, False), 19 | ("11.com", False, False), 20 | ("3.cn.", True, False), 21 | ("_example.com", False, True), 22 | ("example_.com", False, True), 23 | ("_exa_mple_.com", False, True), 24 | ("a.cn", False, False), 25 | ("sub1.sub2.sample.co.uk", False, False), 26 | ("somerandomexample.xn--fiqs8s", False, False), 27 | ("kräuter.com.", True, False), 28 | ("über.com", False, False), 29 | ], 30 | ) 31 | def test_returns_true_on_valid_domain(value: str, rfc_1034: bool, rfc_2782: bool): 32 | """Test returns true on valid domain.""" 33 | assert domain(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782) 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ("value", "consider_tld", "rfc_1034", "rfc_2782"), 38 | [ 39 | ("example.com", True, False, False), 40 | ("exa_mple.com", True, False, True), 41 | ("xn----gtbspbbmkef.xn--p1ai", True, False, False), 42 | ("underscore_subdomain.example.com", True, False, True), 43 | ("someThing.versicherung.", True, True, False), 44 | ("11.com", True, False, False), 45 | ("3.cn.", True, True, False), 46 | ("_example.com", True, False, True), 47 | ("example_.com", True, False, True), 48 | ("somerandomexample.xn--fiqs8s", True, False, False), 49 | ("somerandomexample.onion", True, False, False), 50 | ], 51 | ) 52 | def test_returns_true_on_valid_top_level_domain( 53 | value: str, consider_tld: bool, rfc_1034: bool, rfc_2782: bool 54 | ): 55 | """Test returns true on valid top level domain.""" 56 | assert domain(value, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782) 57 | 58 | 59 | @pytest.mark.parametrize( 60 | ("value", "rfc_1034", "rfc_2782"), 61 | [ 62 | ("example.com/.", True, False), 63 | ("example.com:4444", False, False), 64 | ("example.-com", False, False), 65 | ("example.", False, False), 66 | ("-example.com", False, False), 67 | ("example-.com.", True, False), 68 | ("_example.com", False, False), 69 | ("_example._com", False, False), 70 | ("example_.com", False, False), 71 | ("example", False, False), 72 | ("example.com!", True, False), 73 | ("example?.com", True, False), 74 | ("__exa__mple__.com", False, True), 75 | ("a......b.com", False, False), 76 | ("a.123", False, False), 77 | ("123.123", False, False), 78 | ("123.123.123.", True, False), 79 | ("123.123.123.123", False, False), 80 | ], 81 | ) 82 | def test_returns_failed_validation_on_invalid_domain(value: str, rfc_1034: bool, rfc_2782: bool): 83 | """Test returns failed validation on invalid domain.""" 84 | assert isinstance(domain(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782), ValidationError) 85 | 86 | 87 | @pytest.mark.parametrize( 88 | ("value", "consider_tld", "rfc_1034", "rfc_2782"), 89 | [ 90 | ("example.266", True, False, False), 91 | ("exa_mple.org_", True, False, True), 92 | ("xn----gtbspbbmkef.xn-p1ai", True, False, False), 93 | ("underscore_subdomain.example.flat", True, False, True), 94 | ("someThing.versicherung.reddit.", True, True, False), 95 | ("11.twitter", True, False, False), 96 | ("3.cnx.", True, True, False), 97 | ("_example.#13", True, False, True), 98 | ("example_.fo-ul", True, False, True), 99 | ("somerandomexample.xn-n-fiqs8s", True, False, False), 100 | ], 101 | ) 102 | def test_returns_failed_validation_invalid_top_level_domain( 103 | value: str, consider_tld: bool, rfc_1034: bool, rfc_2782: bool 104 | ): 105 | """Test returns failed validation invalid top level domain.""" 106 | assert isinstance( 107 | domain(value, consider_tld=consider_tld, rfc_1034=rfc_1034, rfc_2782=rfc_2782), 108 | ValidationError, 109 | ) 110 | -------------------------------------------------------------------------------- /tests/test_email.py: -------------------------------------------------------------------------------- 1 | """Test eMail.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, email 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value",), 12 | [ 13 | ("email@here.com",), 14 | ("weirder-email@here.and.there.com",), 15 | ("email@127.local.home.arpa",), 16 | ("example@valid-----hyphens.com",), 17 | ("example@valid-with-hyphens.com",), 18 | ("test@domain.with.idn.tld.उदाहरण.परीक्षा",), 19 | ("email@localhost.in",), 20 | ("Łókaść@email.com",), 21 | ("łemłail@here.com",), 22 | ("email@localdomain.org",), 23 | ('"\\\011"@here.com',), 24 | ], 25 | ) 26 | def test_returns_true_on_valid_email(value: str): 27 | """Test returns true on valid email.""" 28 | assert email(value) 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ("value",), 33 | [ 34 | (None,), 35 | ("",), 36 | ("abc",), 37 | ("abc@",), 38 | ("abc@bar",), 39 | ("a @x.cz",), 40 | ("abc@.com",), 41 | ("something@@somewhere.com",), 42 | ("email@127.0.0.1",), 43 | ("example@invalid-.com",), 44 | ("example@-invalid.com",), 45 | ("example@inv-.alid-.com",), 46 | ("example@inv-.-alid.com",), 47 | ("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",), 48 | ('"test@test"@example.com',), 49 | # Quoted-string format (CR not allowed) 50 | ('"\\\012"@here.com',), 51 | # Non-quoted space/semicolon not allowed 52 | ("stephen smith@example.com",), 53 | ("stephen;smith@example.com",), 54 | ], 55 | ) 56 | def test_returns_failed_validation_on_invalid_email(value: str): 57 | """Test returns failed validation on invalid email.""" 58 | assert isinstance(email(value), ValidationError) 59 | -------------------------------------------------------------------------------- /tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | """Test Encodings.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, base16, base32, base58, base64 8 | 9 | # ==> base16 <== # 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "value", 14 | [ 15 | "a3f4b2", 16 | "01ef", 17 | "abcdef0123456789", 18 | "1234567890abcdef", 19 | "1a2b3c", 20 | "abcdef", 21 | "000102030405060708090A0B0C0D0E0F", 22 | ], 23 | ) 24 | def test_returns_true_on_valid_base16(value: str): 25 | """Test returns true on valid base16.""" 26 | assert base16(value) 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "value", 31 | ["12345g", "hello world", "1234567890abcdeg", "GHIJKL", "12345G", "!@#$%^", "1a2h3c", "a3f4Z1"], 32 | ) 33 | def test_returns_failed_validation_on_invalid_base16(value: str): 34 | """Test returns failed validation on invalid base16.""" 35 | assert isinstance(base16(value), ValidationError) 36 | 37 | 38 | # ==> base32 <== # 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "value", 43 | [ 44 | "JBSWY3DPEHPK3PXP", 45 | "MFRGGZDFMZTWQ2LK", 46 | "MZXW6YTBOI======", 47 | "MFZWIZLTOQ======", 48 | "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 49 | "MFRGGZDFMZTWQ2LKNNWG23Q=", 50 | ], 51 | ) 52 | def test_returns_true_on_valid_base32(value: str): 53 | """Test returns true on valid base32.""" 54 | assert base32(value) 55 | 56 | 57 | @pytest.mark.parametrize( 58 | "value", 59 | [ 60 | "ThisIsNotBase32!", 61 | "12345!", 62 | "Any==invalid=base32=", 63 | "MzXW6yTBOI======", 64 | "JBSWY8DPEHPK9PXP", 65 | "MfZW3zLT9Q======", 66 | ], 67 | ) 68 | def test_returns_failed_validation_on_invalid_base32(value: str): 69 | """Test returns failed validation on invalid base32.""" 70 | assert isinstance(base32(value), ValidationError) 71 | 72 | 73 | # ==> base58 <== # 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "value", 78 | [ 79 | "cUSECaVvAiV3srWbFRvVPzm5YzcXJwPSwZfE7veYPHoXmR9h6YMQ", 80 | "18KToMF5ckjXBYt2HAj77qsG3GPeej3PZn", 81 | "n4FFXRNNEW1aA2WPscSuzHTCjzjs4TVE2Z", 82 | "38XzQ9dPGb1uqbZsjPtUajp7omy8aefjqj", 83 | ], 84 | ) 85 | def test_returns_true_on_valid_base58(value: str): 86 | """Test returns true on valid base58.""" 87 | assert base58(value) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "value", 92 | ["ThisIsAReallyLongStringThatIsDefinitelyNotBase58Encoded", "abcABC!@#", "InvalidBase58!"], 93 | ) 94 | def test_returns_failed_validation_on_invalid_base58(value: str): 95 | """Test returns failed validation on invalid base58.""" 96 | assert isinstance(base58(value), ValidationError) 97 | 98 | 99 | # ==> base64 <== # 100 | 101 | 102 | @pytest.mark.parametrize( 103 | "value", 104 | ["SGVsbG8gV29ybGQ=", "U29tZSBkYXRhIHN0cmluZw==", "YW55IGNhcm5hbCBwbGVhcw=="], 105 | ) 106 | def test_returns_true_on_valid_base64(value: str): 107 | """Test returns true on valid base64.""" 108 | assert base64(value) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | "value", 113 | ["SGVsbG8gV29ybGQ", "U29tZSBkYXRhIHN0cmluZw", "YW55IGNhcm5hbCBwbGVhc"], 114 | ) 115 | def test_returns_failed_validation_on_invalid_base64(value: str): 116 | """Test returns failed validation on invalid base64.""" 117 | assert isinstance(base64(value), ValidationError) 118 | -------------------------------------------------------------------------------- /tests/test_finance.py: -------------------------------------------------------------------------------- 1 | """Test Finance.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, cusip, isin, sedol 8 | 9 | # ==> CUSIP <== # 10 | 11 | 12 | @pytest.mark.parametrize("value", ["912796X38", "912796X20", "912796x20"]) 13 | def test_returns_true_on_valid_cusip(value: str): 14 | """Test returns true on valid cusip.""" 15 | assert cusip(value) 16 | 17 | 18 | @pytest.mark.parametrize("value", ["912796T67", "912796T68", "XCVF", "00^^^1234"]) 19 | def test_returns_failed_validation_on_invalid_cusip(value: str): 20 | """Test returns failed validation on invalid cusip.""" 21 | assert isinstance(cusip(value), ValidationError) 22 | 23 | 24 | # ==> ISIN <== # 25 | 26 | 27 | @pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"]) 28 | def test_returns_true_on_valid_isin(value: str): 29 | """Test returns true on valid isin.""" 30 | assert isin(value) 31 | 32 | 33 | @pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009"]) 34 | def test_returns_failed_validation_on_invalid_isin(value: str): 35 | """Test returns failed validation on invalid isin.""" 36 | assert isinstance(isin(value), ValidationError) 37 | 38 | 39 | # ==> SEDOL <== # 40 | 41 | 42 | @pytest.mark.parametrize("value", ["0263494", "0540528", "B000009"]) 43 | def test_returns_true_on_valid_sedol(value: str): 44 | """Test returns true on valid sedol.""" 45 | assert sedol(value) 46 | 47 | 48 | @pytest.mark.parametrize("value", ["0540526", "XCVF", "00^^^1234", "A000009"]) 49 | def test_returns_failed_validation_on_invalid_sedol(value: str): 50 | """Test returns failed validation on invalid sedol.""" 51 | assert isinstance(sedol(value), ValidationError) 52 | -------------------------------------------------------------------------------- /tests/test_hashes.py: -------------------------------------------------------------------------------- 1 | """Test Hashes.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, base58, base64, md5, sha1, sha224, sha256, sha384, sha512 8 | 9 | # ==> base58 <== # 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "value", 14 | [ 15 | "cUSECaVvAiV3srWbFRvVPzm5YzcXJwPSwZfE7veYPHoXmR9h6YMQ", 16 | "18KToMF5ckjXBYt2HAj77qsG3GPeej3PZn", 17 | "n4FFXRNNEW1aA2WPscSuzHTCjzjs4TVE2Z", 18 | "38XzQ9dPGb1uqbZsjPtUajp7omy8aefjqj", 19 | ], 20 | ) 21 | def test_returns_true_on_valid_base58(value: str): 22 | """Test returns true on valid base58.""" 23 | assert base58(value) 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "value", 28 | ["ThisIsAReallyLongStringThatIsDefinitelyNotBase58Encoded", "abcABC!@#", "InvalidBase58!"], 29 | ) 30 | def test_returns_failed_validation_on_invalid_base58(value: str): 31 | """Test returns failed validation on invalid base58.""" 32 | assert isinstance(base58(value), ValidationError) 33 | 34 | 35 | # ==> base64 <== # 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "value", 40 | ["SGVsbG8gV29ybGQ=", "U29tZSBkYXRhIHN0cmluZw==", "YW55IGNhcm5hbCBwbGVhcw=="], 41 | ) 42 | def test_returns_true_on_valid_base64(value: str): 43 | """Test returns true on valid base64.""" 44 | assert base64(value) 45 | 46 | 47 | @pytest.mark.parametrize( 48 | "value", 49 | ["SGVsbG8gV29ybGQ", "U29tZSBkYXRhIHN0cmluZw", "YW55IGNhcm5hbCBwbGVhc"], 50 | ) 51 | def test_returns_failed_validation_on_invalid_base64(value: str): 52 | """Test returns failed validation on invalid base64.""" 53 | assert isinstance(base64(value), ValidationError) 54 | 55 | 56 | # ==> md5 <== # 57 | 58 | 59 | @pytest.mark.parametrize( 60 | "value", ["d41d8cd98f00b204e9800998ecf8427e", "D41D8CD98F00B204E9800998ECF8427E"] 61 | ) 62 | def test_returns_true_on_valid_md5(value: str): 63 | """Test returns true on valid md5.""" 64 | assert md5(value) 65 | 66 | 67 | @pytest.mark.parametrize( 68 | "value", 69 | [ 70 | "z41d8cd98f00b204e9800998ecf8427e", 71 | "z8cd98f00b204e9800998ecf8427e", 72 | "z4aaaa1d8cd98f00b204e9800998ecf8427e", 73 | ], 74 | ) 75 | def test_returns_failed_validation_on_invalid_md5(value: str): 76 | """Test returns failed validation on invalid md5.""" 77 | assert isinstance(md5(value), ValidationError) 78 | 79 | 80 | # ==> sha1 <== # 81 | 82 | 83 | @pytest.mark.parametrize( 84 | "value", 85 | ["da39a3ee5e6b4b0d3255bfef95601890afd80709", "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"], 86 | ) 87 | def test_returns_true_on_valid_sha1(value: str): 88 | """Test returns true on valid sha1.""" 89 | assert sha1(value) 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "value", 94 | [ 95 | "za39a3ee5e6b4b0d3255bfef95601890afd80709", 96 | "da39e5e6b4b0d3255bfef95601890afd80709", 97 | "daaaa39a3ee5e6b4b0d3255bfef95601890afd80709", 98 | ], 99 | ) 100 | def test_returns_failed_validation_on_invalid_sha1(value: str): 101 | """Test returns failed validation on invalid sha1.""" 102 | assert isinstance(sha1(value), ValidationError) 103 | 104 | 105 | # ==> sha224 <== # 106 | 107 | 108 | @pytest.mark.parametrize( 109 | "value", 110 | [ 111 | "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", 112 | "D14A028C2A3A2BC9476102BB288234C415A2B01F828EA62AC5B3E42F", 113 | ], 114 | ) 115 | def test_returns_true_on_valid_sha224(value: str): 116 | """Test returns true on valid sha224.""" 117 | assert sha224(value) 118 | 119 | 120 | @pytest.mark.parametrize( 121 | "value", 122 | [ 123 | "z14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", 124 | "d028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", 125 | "daaa14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f", 126 | ], 127 | ) 128 | def test_returns_failed_validation_on_invalid_sha224(value: str): 129 | """Test returns failed validation on invalid sha224.""" 130 | assert isinstance(sha224(value), ValidationError) 131 | 132 | 133 | # ==> sha256 <== # 134 | 135 | 136 | @pytest.mark.parametrize( 137 | "value", 138 | [ 139 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 140 | "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", 141 | ], 142 | ) 143 | def test_returns_true_on_valid_sha256(value: str): 144 | """Test returns true on valid sha256.""" 145 | assert sha256(value) 146 | 147 | 148 | @pytest.mark.parametrize( 149 | "value", 150 | [ 151 | "z3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 152 | "ec44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 153 | "eaaaa3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 154 | ], 155 | ) 156 | def test_returns_failed_validation_on_invalid_sha256(value: str): 157 | """Test returns failed validation on invalid sha256.""" 158 | assert isinstance(sha256(value), ValidationError) 159 | 160 | 161 | # ==> sha384 <== # 162 | 163 | 164 | @pytest.mark.parametrize( 165 | "value", 166 | [ 167 | "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", 168 | "CB00753F45A35E8BB5A03D699AC65007272C32AB0EDED1631A8B605A43FF5BED8086072BA1E7CC2358BAECA134C825A7", 169 | "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a", 170 | "F21EF1F8DBF806106813C8504AF864D8D9BFDFA8D67FA9B7DFF1C5B61C2584394A05897C4F157CEEE0E8FBC29205BB8B", 171 | ], 172 | ) 173 | def test_returns_true_on_valid_sha384(value: str): 174 | """Test returns true on valid sha384.""" 175 | assert sha384(value) 176 | 177 | 178 | @pytest.mark.parametrize( 179 | "value", 180 | [ 181 | "zb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", 182 | "c753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", 183 | "cb00aaaa753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", 184 | ], 185 | ) 186 | def test_returns_failed_validation_on_invalid_sha384(value: str): 187 | """Test returns failed validation on invalid sha384.""" 188 | assert isinstance(sha384(value), ValidationError) 189 | 190 | 191 | # ==> sha512 <== # 192 | 193 | 194 | @pytest.mark.parametrize( 195 | "value", 196 | [ 197 | ( 198 | "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d" 199 | "13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" 200 | ), 201 | ( 202 | "CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D" 203 | "13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E" 204 | ), 205 | ], 206 | ) 207 | def test_returns_true_on_valid_sha512(value: str): 208 | """Test returns true on valid sha512.""" 209 | assert sha512(value) 210 | 211 | 212 | @pytest.mark.parametrize( 213 | "value", 214 | [ 215 | ( 216 | "zf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d" 217 | "13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" 218 | ), 219 | ( 220 | "cf8357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c" 221 | "5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" 222 | ), 223 | ( 224 | "cf8aaaa3e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce4" 225 | "7d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" 226 | ), 227 | ], 228 | ) 229 | def test_returns_failed_validation_on_invalid_sha512(value: str): 230 | """Test returns failed validation on invalid sha512.""" 231 | assert isinstance(sha512(value), ValidationError) 232 | -------------------------------------------------------------------------------- /tests/test_hostname.py: -------------------------------------------------------------------------------- 1 | """Test Hostname.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, hostname 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value", "rfc_1034", "rfc_2782"), 12 | [ 13 | # simple hostname w/ optional ports 14 | ("ubuntu-pc:443", False, False), 15 | ("this-pc", False, False), 16 | ("lab-01a-notebook:404", False, False), 17 | ("4-oh-4", False, False), 18 | # hostname w/ optional ports 19 | ("example.com:4444", False, False), 20 | ("kräuter.com.", True, False), 21 | ("xn----gtbspbbmkef.xn--p1ai:65535", False, False), 22 | ("_example.com", False, True), 23 | # ipv4 addr w/ optional ports 24 | ("123.123.123.123:9090", False, False), 25 | ("127.0.0.1:43512", False, False), 26 | ("123.5.77.88:31000", False, False), 27 | ("12.12.12.12:5353", False, False), 28 | # ipv6 addr w/ optional ports 29 | ("[::1]:22", False, False), 30 | ("[dead:beef:0:0:0:0000:42:1]:5731", False, False), 31 | ("[0:0:0:0:0:ffff:1.2.3.4]:80", False, False), 32 | ("[0:a:b:c:d:e:f::]:53", False, False), 33 | ], 34 | ) 35 | def test_returns_true_on_valid_hostname(value: str, rfc_1034: bool, rfc_2782: bool): 36 | """Test returns true on valid hostname.""" 37 | assert hostname(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | ("value", "rfc_1034", "rfc_2782"), 42 | [ 43 | # bad (simple hostname w/ optional ports) 44 | ("ubuntu-pc:443080", False, False), 45 | ("this-pc-is-sh*t", False, False), 46 | ("lab-01a-note._com_.com:404", False, False), 47 | ("4-oh-4:@.com", False, False), 48 | # bad (hostname w/ optional ports) 49 | ("example.com:-4444", False, False), 50 | ("xn----gtbspbbmkef.xn--p1ai:65538", False, False), 51 | ("_example.com:0", False, True), 52 | ("kräuter.com.:81_00", True, False), 53 | # bad (ipv4 addr w/ optional ports) 54 | ("123.123.123.123:99999", False, False), 55 | ("127.0.0.1:", False, False), 56 | ("123.5.-12.88:8080", False, False), 57 | ("12.12.12.12:$#", False, False), 58 | # bad (ipv6 addr w/ optional ports) 59 | ("[::1]:[22]", False, False), 60 | ("[dead:beef:0:-:0:-:42:1]:5731", False, False), 61 | ("[0:0:0:0:0:ffff:1.2.3.4]:-65538", False, False), 62 | ("[0:&:b:c:@:e:f:::9999", False, False), 63 | ], 64 | ) 65 | def test_returns_failed_validation_on_invalid_hostname(value: str, rfc_1034: bool, rfc_2782: bool): 66 | """Test returns failed validation on invalid hostname.""" 67 | assert isinstance(hostname(value, rfc_1034=rfc_1034, rfc_2782=rfc_2782), ValidationError) 68 | -------------------------------------------------------------------------------- /tests/test_iban.py: -------------------------------------------------------------------------------- 1 | """Test IBAN.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, iban 8 | 9 | 10 | @pytest.mark.parametrize("value", ["GB82WEST12345698765432", "NO9386011117947"]) 11 | def test_returns_true_on_valid_iban(value: str): 12 | """Test returns true on valid iban.""" 13 | assert iban(value) 14 | 15 | 16 | @pytest.mark.parametrize("value", ["GB81WEST12345698765432", "NO9186011117947"]) 17 | def test_returns_failed_validation_on_invalid_iban(value: str): 18 | """Test returns failed validation on invalid iban.""" 19 | assert isinstance(iban(value), ValidationError) 20 | -------------------------------------------------------------------------------- /tests/test_ip_address.py: -------------------------------------------------------------------------------- 1 | """Test IP Address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, ipv4, ipv6 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("address",), 12 | [ 13 | ("127.0.0.1",), 14 | ("123.5.77.88",), 15 | ("12.12.12.12",), 16 | ], 17 | ) 18 | def test_returns_true_on_valid_ipv4_address(address: str): 19 | """Test returns true on valid ipv4 address.""" 20 | assert ipv4(address) 21 | assert not ipv6(address) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ("address", "cidr", "strict", "host_bit"), 26 | [ 27 | ("127.0.0.1/0", True, True, True), 28 | ("123.5.77.88", True, False, True), 29 | ("12.12.12.0/24", True, True, False), 30 | ], 31 | ) 32 | def test_returns_true_on_valid_ipv4_cidr_address( 33 | address: str, cidr: bool, strict: bool, host_bit: bool 34 | ): 35 | """Test returns true on valid ipv4 CIDR address.""" 36 | assert ipv4(address, cidr=cidr, strict=strict, host_bit=host_bit) 37 | assert not ipv6(address, cidr=cidr, strict=strict, host_bit=host_bit) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | ("address",), 42 | [ 43 | # leading zeroes error-out from Python 3.9.5 44 | # ("100.100.033.033",), 45 | ("900.200.100.75",), 46 | ("0127.0.0.1",), 47 | ("abc.0.0.1",), 48 | ], 49 | ) 50 | def test_returns_failed_validation_on_invalid_ipv4_address(address: str): 51 | """Test returns failed validation on invalid ipv4 address.""" 52 | assert isinstance(ipv4(address), ValidationError) 53 | 54 | 55 | @pytest.mark.parametrize( 56 | ("address", "cidr", "strict", "host_bit"), 57 | [ 58 | ("1.1.1.1/1", False, True, True), 59 | ("1.1.1.1/33", True, False, True), 60 | ("1.1.1.1/24", True, True, False), 61 | ("1.1.1.1/-1", True, True, True), 62 | ], 63 | ) 64 | def test_returns_failed_validation_on_invalid_ipv4_cidr_address( 65 | address: str, cidr: bool, strict: bool, host_bit: bool 66 | ): 67 | """Test returns failed validation on invalid ipv4 CIDR address.""" 68 | assert isinstance(ipv4(address, cidr=cidr, strict=strict, host_bit=host_bit), ValidationError) 69 | 70 | 71 | @pytest.mark.parametrize( 72 | ("address",), 73 | [ 74 | ("::",), 75 | ("::1",), 76 | ("1::",), 77 | ("dead:beef:0:0:0:0000:42:1",), 78 | ("abcd:ef::42:1",), 79 | ("0:0:0:0:0:ffff:1.2.3.4",), 80 | ("::192.168.30.2",), 81 | ("0000:0000:0000:0000:0000::",), 82 | ("0:a:b:c:d:e:f::",), 83 | ], 84 | ) 85 | def test_returns_true_on_valid_ipv6_address(address: str): 86 | """Test returns true on valid ipv6 address.""" 87 | assert ipv6(address) 88 | assert not ipv4(address) 89 | 90 | 91 | @pytest.mark.parametrize( 92 | ("address", "cidr", "strict", "host_bit"), 93 | [ 94 | ("::1/128", True, True, True), 95 | ("::1/0", True, True, True), 96 | ("dead:beef:0:0:0:0:42:1/8", True, True, True), 97 | ("abcd:ef::42:1/32", True, True, True), 98 | ("0:0:0:0:0:ffff:1.2.3.4/16", True, True, True), 99 | ("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64", True, True, True), 100 | ("::192.168.30.2/128", True, True, True), 101 | ], 102 | ) 103 | def test_returns_true_on_valid_ipv6_cidr_address( 104 | address: str, cidr: bool, strict: bool, host_bit: bool 105 | ): 106 | """Test returns true on valid ipv6 CIDR address.""" 107 | assert ipv6(address, cidr=cidr, strict=strict, host_bit=host_bit) 108 | assert not ipv4(address, cidr=cidr, strict=strict, host_bit=host_bit) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | ("address",), 113 | [ 114 | ("abc.0.0.1",), 115 | ("abcd:1234::123::1",), 116 | ("1:2:3:4:5:6:7:8:9",), 117 | ("1:2:3:4:5:6:7:8::",), 118 | ("1:2:3:4:5:6:7::8:9",), 119 | ("abcd::1ffff",), 120 | ("1111:",), 121 | (":8888",), 122 | (":1.2.3.4",), 123 | ("18:05",), 124 | (":",), 125 | (":1:2:",), 126 | (":1:2::",), 127 | ("::1:2::",), 128 | ("8::1:2::9",), 129 | ("02001:0000:1234:0000:0000:C1C0:ABCD:0876",), 130 | ], 131 | ) 132 | def test_returns_failed_validation_on_invalid_ipv6_address(address: str): 133 | """Test returns failed validation on invalid ipv6 address.""" 134 | assert isinstance(ipv6(address), ValidationError) 135 | 136 | 137 | @pytest.mark.parametrize( 138 | ("address", "cidr", "strict", "host_bit"), 139 | [ 140 | ("::1/128", False, True, True), 141 | ("::1/129", True, False, True), 142 | ("dead:beef:0:0:0:0:42:1/8", True, True, False), 143 | ("::1/-130", True, True, True), 144 | ], 145 | ) 146 | def test_returns_failed_validation_on_invalid_ipv6_cidr_address( 147 | address: str, cidr: bool, strict: bool, host_bit: bool 148 | ): 149 | """Test returns failed validation on invalid ipv6 CIDR address.""" 150 | assert isinstance(ipv6(address, cidr=cidr, strict=strict, host_bit=host_bit), ValidationError) 151 | 152 | 153 | @pytest.mark.parametrize( 154 | ("address", "private"), 155 | [ 156 | ("10.1.1.1", True), 157 | ("192.168.1.1", True), 158 | ("169.254.1.1", True), 159 | ("127.0.0.1", True), 160 | ("0.0.0.0", True), 161 | ], 162 | ) 163 | def test_returns_true_on_valid_private_ipv4_address(address: str, private: bool): 164 | """Test returns true on private ipv4 address.""" 165 | assert ipv4(address, private=private) 166 | 167 | 168 | @pytest.mark.parametrize( 169 | ("address", "private"), 170 | [ 171 | ("1.1.1.1", True), 172 | ("192.169.1.1", True), 173 | ("7.53.12.1", True), 174 | ], 175 | ) 176 | def test_returns_failed_validation_on_invalid_private_ipv4_address(address: str, private: bool): 177 | """Test returns failed validation on invalid private ipv4 address.""" 178 | assert isinstance(ipv4(address, private=private), ValidationError) 179 | 180 | 181 | @pytest.mark.parametrize( 182 | ("address", "private"), 183 | [ 184 | ("1.1.1.1", False), 185 | ("192.169.1.1", False), 186 | ("7.53.12.1", False), 187 | ], 188 | ) 189 | def test_returns_true_on_valid_public_ipv4_address(address: str, private: bool): 190 | """Test returns true on valid public ipv4 address.""" 191 | assert ipv4(address, private=private) 192 | 193 | 194 | @pytest.mark.parametrize( 195 | ("address", "private"), 196 | [ 197 | ("10.1.1.1", False), 198 | ("192.168.1.1", False), 199 | ("169.254.1.1", False), 200 | ("127.0.0.1", False), 201 | ("0.0.0.0", False), 202 | ], 203 | ) 204 | def test_returns_failed_validation_on_invalid_public_ipv4_address(address: str, private: bool): 205 | """Test returns failed validation on private ipv4 address.""" 206 | assert isinstance(ipv4(address, private=private), ValidationError) 207 | -------------------------------------------------------------------------------- /tests/test_length.py: -------------------------------------------------------------------------------- 1 | """Test Length.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, length 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("value", "min_val", "max_val"), 12 | [("password", 2, None), ("password", None, None), ("password", 0, 10), ("password", 8, 8)], 13 | ) 14 | def test_returns_true_on_valid_length(value: str, min_val: int, max_val: int): 15 | """Test returns true on valid length.""" 16 | assert length(value, min_val=min_val, max_val=max_val) 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ("value", "min_val", "max_val"), 21 | [("something", 14, 12), ("something", -10, -20), ("something", 0, -2), ("something", 13, 14)], 22 | ) 23 | def test_returns_failed_validation_on_invalid_range(value: str, min_val: int, max_val: int): 24 | """Test returns failed validation on invalid range.""" 25 | assert isinstance(length(value, min_val=min_val, max_val=max_val), ValidationError) 26 | -------------------------------------------------------------------------------- /tests/test_mac_address.py: -------------------------------------------------------------------------------- 1 | """MAC Address.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, mac_address 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ("address",), 12 | [ 13 | ("01:23:45:67:ab:CD",), 14 | ("01-23-45-67-ab-CD",), 15 | ("01:2F:45:37:ab:CD",), 16 | ("A1-2F-4E-68-ab-CD",), 17 | ], 18 | ) 19 | def test_returns_true_on_valid_mac_address(address: str): 20 | """Test returns true on valid mac address.""" 21 | assert mac_address(address) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ("address",), 26 | [ 27 | ("00-00:-00-00-00",), 28 | ("01:23:45:67:89:",), 29 | ("01:23-45:67-89:gh",), 30 | ("123:23:45:67:89:00",), 31 | ], 32 | ) 33 | def test_returns_failed_validation_on_invalid_mac_address(address: str): 34 | """Test returns failed validation on invalid mac address.""" 35 | assert isinstance(mac_address(address), ValidationError) 36 | -------------------------------------------------------------------------------- /tests/test_slug.py: -------------------------------------------------------------------------------- 1 | """Test Slug.""" 2 | 3 | # external 4 | import pytest 5 | 6 | # local 7 | from validators import ValidationError, slug 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "value", 12 | [ 13 | "123-asd-7sda", 14 | "123-k-123", 15 | "dac-12sa-459", 16 | "dac-12sa7-ad31as", 17 | ], 18 | ) 19 | def test_returns_true_on_valid_slug(value: str): 20 | """Test returns true on valid slug.""" 21 | assert slug(value) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "value", 26 | [ 27 | "some.slug&", 28 | "1231321%", 29 | " 21312", 30 | "-47q-p--123", 31 | ], 32 | ) 33 | def test_returns_failed_validation_on_invalid_slug(value: str): 34 | """Test returns failed validation on invalid slug.""" 35 | assert isinstance(slug(value), ValidationError) 36 | -------------------------------------------------------------------------------- /tests/test_uuid.py: -------------------------------------------------------------------------------- 1 | """Test UUIDs.""" 2 | 3 | # standard 4 | from typing import Union 5 | from uuid import UUID, uuid4 6 | 7 | # external 8 | import pytest 9 | 10 | # local 11 | from validators import ValidationError, uuid 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("value",), 16 | [ 17 | (uuid4(),), 18 | ("2bc1c94f-0deb-43e9-92a1-4775189ec9f8",), 19 | (uuid4(),), 20 | ("888256d7c49341f19fa33f29d3f820d7",), 21 | ], 22 | ) 23 | def test_returns_true_on_valid_uuid(value: Union[str, UUID]): 24 | """Test returns true on valid uuid.""" 25 | assert uuid(value) 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ("value",), 30 | [ 31 | ("2bc1c94f-deb-43e9-92a1-4775189ec9f8",), 32 | ("2bc1c94f-0deb-43e9-92a1-4775189ec9f",), 33 | ("gbc1c94f-0deb-43e9-92a1-4775189ec9f8",), 34 | ("2bc1c94f 0deb-43e9-92a1-4775189ec9f8",), 35 | ], 36 | ) 37 | def test_returns_failed_validation_on_invalid_uuid(value: Union[str, UUID]): 38 | """Test returns failed validation on invalid uuid.""" 39 | assert isinstance(uuid(value), ValidationError) 40 | -------------------------------------------------------------------------------- /tests/test_validation_failure.py: -------------------------------------------------------------------------------- 1 | """Test validation Failure.""" 2 | 3 | # local 4 | from validators import between 5 | 6 | failed_obj_repr = "ValidationError(func=between" 7 | 8 | 9 | class TestValidationError: 10 | """Test validation Failure.""" 11 | 12 | def setup_method(self): 13 | """Setup Method.""" 14 | self.is_in_between = between(3, min_val=4, max_val=5) 15 | 16 | def test_boolean_coerce(self): 17 | """Test Boolean.""" 18 | assert not bool(self.is_in_between) 19 | assert not self.is_in_between 20 | 21 | def test_repr(self): 22 | """Test Repr.""" 23 | assert failed_obj_repr in repr(self.is_in_between) 24 | 25 | def test_string(self): 26 | """Test Repr.""" 27 | assert failed_obj_repr in str(self.is_in_between) 28 | 29 | def test_arguments_as_properties(self): 30 | """Test argument properties.""" 31 | assert self.is_in_between.__dict__["value"] == 3 32 | assert self.is_in_between.__dict__["min_val"] == 4 33 | assert self.is_in_between.__dict__["max_val"] == 5 34 | --------------------------------------------------------------------------------