├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── coverage.yml │ ├── github-actions.yml │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ci ├── bootstrap.py ├── requirements.txt └── templates │ ├── .appveyor.yml │ └── .github │ └── workflows │ └── github-actions.yml ├── docs ├── authors.rst ├── built-in │ └── index.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── readme.rst ├── regex-builder │ ├── builder │ │ └── index.rst │ ├── flags │ │ └── index.rst │ └── index.rst ├── requirements.txt └── spelling_wordlist.txt ├── images ├── cover.png ├── logo_1500px.png ├── logo_500px.png ├── logo_no_text_1500px.png ├── logo_no_text_500px.png ├── logo_no_text_black_1500px.png ├── logo_no_text_black_500px.png ├── logo_no_text_t_1500px.png ├── logo_no_text_t_500px.png ├── logo_no_text_t_black_1500px.png ├── logo_no_text_t_black_500px.png ├── logo_t_1500px.png ├── logo_t_500px.png ├── logo_t_black_1500px.png ├── logo_t_black_500px.png ├── logo_wob_1500px.png └── logo_wob_500px.png ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py ├── src └── edify │ ├── __init__.py │ ├── builder │ ├── builder.py │ ├── errors.py │ └── helpers │ │ ├── core.py │ │ ├── quantifiers.py │ │ ├── regex_vars.py │ │ └── t.py │ └── library │ ├── __init__.py │ ├── date.py │ ├── guid.py │ ├── ip.py │ ├── mac.py │ ├── mail.py │ ├── password.py │ ├── phone.py │ ├── ssn.py │ ├── support │ └── zip.py │ ├── url.py │ ├── uuid.py │ └── zip.py ├── tests.local.sh ├── tests ├── ssn_test.py ├── test_builder.py ├── test_date.py ├── test_email.py ├── test_guid.py ├── test_ip.py ├── test_mac.py ├── test_password.py ├── test_phone.py ├── test_url.py ├── test_uuid.py └── test_zip.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file (badge):README.rst] 11 | search = /v{current_version}.svg 12 | replace = /v{new_version}.svg 13 | 14 | [bumpversion:file (link):README.rst] 15 | search = /v{current_version}...main 16 | replace = /v{new_version}...main 17 | 18 | [bumpversion:file:docs/conf.py] 19 | search = version = release = '{current_version}' 20 | replace = version = release = '{new_version}' 21 | 22 | [bumpversion:file:src/edify/__init__.py] 23 | search = __version__ = '{current_version}' 24 | replace = __version__ = '{new_version}' 25 | 26 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary edify 9 | # 10 | # See: 11 | # https://pypi.org/project/cookiepatcher 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=edify/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | _extensions: ['jinja2_time.TimeExtension'] 20 | _template: 'gh:ionelmc/cookiecutter-pylibrary' 21 | allow_tests_inside_package: 'no' 22 | appveyor: 'no' 23 | c_extension_function: 'longest' 24 | c_extension_module: '_edify' 25 | c_extension_optional: 'no' 26 | c_extension_support: 'no' 27 | c_extension_test_pypi: 'no' 28 | c_extension_test_pypi_username: 'luciferreeves' 29 | codacy: 'no' 30 | codacy_projectid: '[Get ID from https://app.codacy.com/gh/luciferreeves/edify/settings]' 31 | codeclimate: 'no' 32 | codecov: 'yes' 33 | command_line_interface: 'no' 34 | command_line_interface_bin_name: 'edify' 35 | coveralls: 'no' 36 | distribution_name: 'edify' 37 | email: 'bobbyskhs@gmail.com' 38 | full_name: 'Bobby' 39 | github_actions: 'yes' 40 | github_actions_osx: 'yes' 41 | github_actions_windows: 'yes' 42 | legacy_python: 'no' 43 | license: 'Apache Software License 2.0' 44 | linter: 'flake8' 45 | package_name: 'edify' 46 | pre_commit: 'yes' 47 | pre_commit_formatter: 'black' 48 | project_name: 'Edify' 49 | project_short_description: 'Regular Expressions Made Simple' 50 | pypi_badge: 'yes' 51 | pypi_disable_upload: 'no' 52 | release_date: 'today' 53 | repo_hosting: 'github.com' 54 | repo_hosting_domain: 'github.com' 55 | repo_main_branch: 'main' 56 | repo_name: 'edify' 57 | repo_username: 'luciferreeves' 58 | requiresio: 'yes' 59 | scrutinizer: 'no' 60 | setup_py_uses_pytest_runner: 'no' 61 | setup_py_uses_setuptools_scm: 'no' 62 | sphinx_docs: 'yes' 63 | sphinx_docs_hosting: 'https://edify.readthedocs.io/' 64 | sphinx_doctest: 'no' 65 | sphinx_theme: 'sphinx-rtd-theme' 66 | test_matrix_configurator: 'no' 67 | test_matrix_separate_coverage: 'no' 68 | travis: 'no' 69 | travis_osx: 'no' 70 | version: '0.1.0' 71 | version_manager: 'bump2version' 72 | website: 'https://thatcomputerscientist.com' 73 | year_from: '2022' 74 | year_to: '2022' 75 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src 4 | */site-packages 5 | 6 | [run] 7 | branch = true 8 | source = 9 | edify 10 | tests 11 | parallel = true 12 | 13 | [report] 14 | show_missing = true 15 | precision = 2 16 | omit = *migrations* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | # Use Unix-style newlines for most files (except Windows files, see below). 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | insert_final_newline = true 10 | indent_size = 4 11 | charset = utf-8 12 | 13 | [*.{bat,cmd,ps1}] 14 | end_of_line = crlf 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | 19 | [*.tsv] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main", "dev" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main", "dev" ] 20 | schedule: 21 | - cron: '0 0 * * *' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Upload Coverage Reports 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | timeout-minutes: 30 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-python@v3 10 | with: 11 | python-version: 3.11 12 | - name: install dependencies 13 | run: | 14 | python -m pip install --upgrade pip 15 | python -mpip install --progress-bar=off -r ci/requirements.txt 16 | virtualenv --version 17 | pip --version 18 | tox --version 19 | pip list --format=freeze 20 | - name: upload coverage reports 21 | run: | 22 | tox -e clean -v 23 | tox -e py311 -v 24 | tox -e report -v 25 | tox -e codecov -v 26 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: ${{ matrix.name }} 6 | runs-on: ${{ matrix.os }} 7 | timeout-minutes: 30 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - name: 'check' 13 | python: '3.9' 14 | toxpython: 'python3.9' 15 | tox_env: 'check' 16 | os: 'ubuntu-latest' 17 | - name: 'docs' 18 | python: '3.9' 19 | toxpython: 'python3.9' 20 | tox_env: 'docs' 21 | os: 'ubuntu-latest' 22 | - name: 'py37 (ubuntu)' 23 | python: '3.7' 24 | toxpython: 'python3.7' 25 | python_arch: 'x64' 26 | tox_env: 'py37' 27 | os: 'ubuntu-latest' 28 | - name: 'py37 (windows)' 29 | python: '3.7' 30 | toxpython: 'python3.7' 31 | python_arch: 'x64' 32 | tox_env: 'py37' 33 | os: 'windows-latest' 34 | - name: 'py37 (macos)' 35 | python: '3.7' 36 | toxpython: 'python3.7' 37 | python_arch: 'x64' 38 | tox_env: 'py37' 39 | os: 'macos-latest' 40 | - name: 'py38 (ubuntu)' 41 | python: '3.8' 42 | toxpython: 'python3.8' 43 | python_arch: 'x64' 44 | tox_env: 'py38' 45 | os: 'ubuntu-latest' 46 | - name: 'py38 (windows)' 47 | python: '3.8' 48 | toxpython: 'python3.8' 49 | python_arch: 'x64' 50 | tox_env: 'py38' 51 | os: 'windows-latest' 52 | - name: 'py38 (macos)' 53 | python: '3.8' 54 | toxpython: 'python3.8' 55 | python_arch: 'x64' 56 | tox_env: 'py38' 57 | os: 'macos-latest' 58 | - name: 'py39 (ubuntu)' 59 | python: '3.9' 60 | toxpython: 'python3.9' 61 | python_arch: 'x64' 62 | tox_env: 'py39' 63 | os: 'ubuntu-latest' 64 | - name: 'py39 (windows)' 65 | python: '3.9' 66 | toxpython: 'python3.9' 67 | python_arch: 'x64' 68 | tox_env: 'py39' 69 | os: 'windows-latest' 70 | - name: 'py39 (macos)' 71 | python: '3.9' 72 | toxpython: 'python3.9' 73 | python_arch: 'x64' 74 | tox_env: 'py39' 75 | os: 'macos-latest' 76 | - name: 'py310 (ubuntu)' 77 | python: '3.10' 78 | toxpython: 'python3.10' 79 | python_arch: 'x64' 80 | tox_env: 'py310' 81 | os: 'ubuntu-latest' 82 | - name: 'py310 (windows)' 83 | python: '3.10' 84 | toxpython: 'python3.10' 85 | python_arch: 'x64' 86 | tox_env: 'py310' 87 | os: 'windows-latest' 88 | - name: 'py310 (macos)' 89 | python: '3.10' 90 | toxpython: 'python3.10' 91 | python_arch: 'x64' 92 | tox_env: 'py310' 93 | os: 'macos-latest' 94 | - name: 'py311 (ubuntu)' 95 | python: '3.11' 96 | toxpython: 'python3.11' 97 | python_arch: 'x64' 98 | tox_env: 'py311' 99 | os: 'ubuntu-latest' 100 | - name: 'py311 (windows)' 101 | python: '3.11' 102 | toxpython: 'python3.11' 103 | python_arch: 'x64' 104 | tox_env: 'py311' 105 | os: 'windows-latest' 106 | - name: 'py311 (macos)' 107 | python: '3.11' 108 | toxpython: 'python3.11' 109 | python_arch: 'x64' 110 | tox_env: 'py311' 111 | os: 'macos-latest' 112 | - name: 'pypy37 (ubuntu)' 113 | python: 'pypy-3.7' 114 | toxpython: 'pypy3.7' 115 | python_arch: 'x64' 116 | tox_env: 'pypy37' 117 | os: 'ubuntu-latest' 118 | - name: 'pypy37 (windows)' 119 | python: 'pypy-3.7' 120 | toxpython: 'pypy3.7' 121 | python_arch: 'x64' 122 | tox_env: 'pypy37' 123 | os: 'windows-latest' 124 | - name: 'pypy37 (macos)' 125 | python: 'pypy-3.7' 126 | toxpython: 'pypy3.7' 127 | python_arch: 'x64' 128 | tox_env: 'pypy37' 129 | os: 'macos-latest' 130 | - name: 'pypy38 (ubuntu)' 131 | python: 'pypy-3.8' 132 | toxpython: 'pypy3.8' 133 | python_arch: 'x64' 134 | tox_env: 'pypy38' 135 | os: 'ubuntu-latest' 136 | - name: 'pypy38 (windows)' 137 | python: 'pypy-3.8' 138 | toxpython: 'pypy3.8' 139 | python_arch: 'x64' 140 | tox_env: 'pypy38' 141 | os: 'windows-latest' 142 | - name: 'pypy38 (macos)' 143 | python: 'pypy-3.8' 144 | toxpython: 'pypy3.8' 145 | python_arch: 'x64' 146 | tox_env: 'pypy38' 147 | os: 'macos-latest' 148 | steps: 149 | - uses: actions/checkout@v3 150 | with: 151 | fetch-depth: 0 152 | - uses: actions/setup-python@v3 153 | with: 154 | python-version: ${{ matrix.python }} 155 | architecture: ${{ matrix.python_arch }} 156 | - name: install dependencies 157 | run: | 158 | python -mpip install --progress-bar=off -r ci/requirements.txt 159 | virtualenv --version 160 | pip --version 161 | tox --version 162 | pip list --format=freeze 163 | - name: test 164 | env: 165 | TOXPYTHON: '${{ matrix.toxpython }}' 166 | run: > 167 | tox -e ${{ matrix.tox_env }} -v 168 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | wheelhouse 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | venv*/ 24 | pyvenv*/ 25 | pip-wheel-metadata/ 26 | 27 | # Installer logs 28 | pip-log.txt 29 | 30 | # Unit test / coverage reports 31 | .coverage 32 | .tox 33 | .coverage.* 34 | .pytest_cache/ 35 | nosetests.xml 36 | coverage.xml 37 | htmlcov 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Buildout 43 | .mr.developer.cfg 44 | 45 | # IDE project files 46 | .project 47 | .pydevproject 48 | .idea 49 | .vscode 50 | *.iml 51 | *.komodoproject 52 | 53 | # Complexity 54 | output/*.html 55 | output/*/index.html 56 | 57 | # Sphinx 58 | docs/_build 59 | 60 | .DS_Store 61 | *~ 62 | .*.sw[po] 63 | .build 64 | .ve 65 | .env 66 | .cache 67 | .pytest 68 | .benchmarks 69 | .bootstrap 70 | .appveyor.token 71 | *.bak 72 | 73 | # Mypy Cache 74 | .mypy_cache/ 75 | 76 | # Codecov Binary 77 | codecov 78 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # To install the git pre-commit hook run: 2 | # pre-commit install 3 | # To update the pre-commit hooks run: 4 | # pre-commit install-hooks 5 | exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: main 9 | hooks: 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | - id: debug-statements 13 | - repo: https://github.com/timothycrosley/isort 14 | rev: main 15 | hooks: 16 | - id: isort 17 | - repo: https://github.com/psf/black 18 | rev: main 19 | hooks: 20 | - id: black 21 | - repo: https://gitlab.com/pycqa/flake8 22 | rev: master 23 | hooks: 24 | - id: flake8 25 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | version: 2 3 | sphinx: 4 | configuration: docs/conf.py 5 | formats: all 6 | python: 7 | install: 8 | - requirements: docs/requirements.txt 9 | - method: pip 10 | path: . 11 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Bobby - https://thatcomputerscientist.com 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 0.2.2 (2022-12-22) 5 | ------------------ 6 | * Added Support for Python 3.11 7 | * Added more RegexBuilder Examples 8 | * Fixed Documentation Typos 9 | 10 | 0.2.1 (2022-11-27) 11 | ------------------ 12 | 13 | * This is a Quick Fix Release to fix the incomplete release of 0.2.0. The release was intended to drop support for 3.6, but the metadata was not updated to reflect this. This release fixes that. v0.2.0 remains available on PyPI, but is incompatible with Python 3.6. Using it with other versions of Python is not a problem. Other than the metadata, the two releases are identical. 14 | 15 | 0.2.0 (2022-11-27) 16 | ------------------ 17 | This is a minor release with a few new built-in validators along with some small changes and bug fixes. 18 | 19 | Validators added: 20 | ~~~~~~~~~~~~~~~~~ 21 | * URL Validator 22 | * UUID Validator 23 | * GUID Validator 24 | * SSN Validator 25 | * Mac Address (IEEE 802) Validator 26 | * Zip Code Validator 27 | * Password Validator 28 | 29 | Documentation: 30 | ~~~~~~~~~~~~~~ 31 | 32 | * Added documentation for new validators 33 | * Add warning for trade-offs in email regex validation 34 | 35 | Bug Fixes: 36 | ~~~~~~~~~~ 37 | 38 | * Fixed Phone pattern failing for service numbers and 4 digit numbers (See `#16 `_ for more information) 39 | 40 | 41 | 0.1.0 (2022-09-10) 42 | ------------------ 43 | 44 | * First release on PyPI. 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | Edify could always use more documentation, whether as part of the 21 | official Edify docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/luciferreeves/edify/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `edify` for local development: 39 | 40 | 1. Fork `edify `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:YOURGITHUBNAME/edify.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: 53 | 54 | tox 55 | 56 | (Optional) If you're using a UNIX Like OS you can run the tests by running the following shell script:: 57 | 58 | ./tests.local.sh 59 | 60 | 5. Commit your changes and push your branch to GitHub:: 61 | 62 | git add . 63 | git commit -m "Your detailed description of your changes." 64 | git push origin name-of-your-bugfix-or-feature 65 | 66 | 6. Submit a pull request through the GitHub website. 67 | 68 | Pull Request Guidelines 69 | ----------------------- 70 | 71 | If you need some code review or feedback while you're developing the code just make the pull request. 72 | 73 | For merging, you should: 74 | 75 | 1. Include passing tests (run ``tox``). 76 | 2. Update documentation when there's new API, functionality etc. 77 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 78 | 4. Add yourself to ``AUTHORS.rst``. 79 | 80 | 81 | 82 | Tips 83 | ---- 84 | 85 | To run a subset of tests:: 86 | 87 | tox -e envname -- pytest -k test_myfeature 88 | 89 | To run all the test environments in *parallel*:: 90 | 91 | tox -p auto 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft src 3 | graft ci 4 | graft tests 5 | 6 | include .bumpversion.cfg 7 | include .cookiecutterrc 8 | include .coveragerc 9 | include .editorconfig 10 | include .github/workflows/github-actions.yml 11 | include .pre-commit-config.yaml 12 | include .readthedocs.yml 13 | include pytest.ini 14 | include tox.ini 15 | 16 | include AUTHORS.rst 17 | include CHANGELOG.rst 18 | include CONTRIBUTING.rst 19 | include LICENSE 20 | include README.rst 21 | include *.sh 22 | recursive-include images *.png 23 | 24 | global-exclude *.py[cod] __pycache__/* *.so *.dylib 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Edify 3 | ======== 4 | 5 | .. Cover Image 6 | .. image:: https://raw.githubusercontent.com/luciferreeves/edify/main/images/cover.png 7 | :alt: Cover Image 8 | 9 | | 10 | 11 | .. image:: https://readthedocs.org/projects/edify/badge/?style=flat&version=latest 12 | :target: https://edify.readthedocs.io/ 13 | :alt: Documentation Status 14 | 15 | .. image:: https://github.com/luciferreeves/edify/actions/workflows/github-actions.yml/badge.svg?branch=main 16 | :alt: GitHub Actions Build Status 17 | :target: https://github.com/luciferreeves/edify/actions 18 | 19 | .. image:: https://codecov.io/gh/luciferreeves/edify/branch/main/graphs/badge.svg?branch=main 20 | :alt: Coverage Status 21 | :target: https://codecov.io/github/luciferreeves/edify 22 | 23 | .. image:: https://img.shields.io/pypi/v/edify.svg 24 | :alt: PyPI Package latest release 25 | :target: https://pypi.org/project/edify 26 | 27 | .. image:: https://img.shields.io/pypi/wheel/edify.svg 28 | :alt: PyPI Wheel 29 | :target: https://pypi.org/project/edify 30 | 31 | .. image:: https://img.shields.io/pypi/pyversions/edify.svg 32 | :alt: Supported versions 33 | :target: https://pypi.org/project/edify 34 | 35 | .. image:: https://img.shields.io/pypi/implementation/edify.svg 36 | :alt: Supported implementations 37 | :target: https://pypi.org/project/edify 38 | 39 | .. image:: https://img.shields.io/github/commits-since/luciferreeves/edify/v0.2.2.svg 40 | :alt: Commits since latest release 41 | :target: https://github.com/luciferreeves/edify/compare/v0.2.2...main 42 | 43 | 44 | 45 | .. end-badges 46 | 47 | | 48 | 49 | Edify (/ˈɛdɪfaɪ/, "ed-uh-fahy") is a Python library that allows you to easily create regular expressions for matching text in a programmatically-friendly way. It is designed to be used in conjunction with the ``re`` module. 50 | 51 | It also allows you to verify a string quickly by providing commonly used regex patterns in its extensive set of built-in patterns. To tap into a pattern, simply import the pattern function from the ``edify.library`` module. 52 | 53 | Quick Start 54 | ============= 55 | 56 | To get started make sure you have python 3.7 or later installed and then, install Edify from ``pip``: 57 | 58 | .. code-block:: bash 59 | 60 | pip install edify 61 | 62 | You can also install the in-development version with: 63 | 64 | .. code-block:: bash 65 | 66 | pip install https://github.com/luciferreeves/edify/archive/main.zip 67 | 68 | Then go on to import the ``RegexBuilder`` class from the ``edify`` module. 69 | 70 | Using Pre-Built Patterns 71 | ------------------------ 72 | 73 | The following example recognises and captures any email like ``email@example.com``. 74 | 75 | .. code-block:: python 76 | 77 | 78 | from edify.library import email 79 | 80 | email_addr = "email@example.com" 81 | assert email(email_addr) == True 82 | 83 | 84 | Building Regex Example: Validating 16-bit Hexadecimal Number 85 | ------------------------------------------------------------ 86 | 87 | The following example recognises and captures the value of a 16-bit hexadecimal number like ``0xC0D3``. 88 | 89 | .. code-block:: python 90 | 91 | 92 | from edify import RegexBuilder 93 | 94 | expr = ( 95 | RegexBuilder() 96 | .start_of_input() 97 | .optional().string("0x") # match an optional "0x" string 98 | .capture() 99 | .exactly(4).any_of() # let x = any characters between (a-f, A-F, and 0-9) 100 | .range("A", "F") # now capture exactly 4 such groups of "x" 101 | .range("a", "f") # this will give the match for the number like "C0D3" 102 | .range("0", "9") # which when combined with "0x" becomes a 16-bit hexadecimal number 103 | .end() 104 | .end() 105 | .end_of_input() 106 | .to_regex() # used to convert to `re` compatible form 107 | ) 108 | 109 | """ 110 | Produces the following regular expression: 111 | re.compile(^(?:0x)?([A-Fa-f0-9]{4})$) 112 | 113 | Using `to_regex_string()` instead of `to_regex()` at the end 114 | will give the compiled regex string. 115 | """ 116 | 117 | assert expr.match("0xC0D3") 118 | 119 | Building Regex Example: Validating Signed Integer 120 | ------------------------------------------------- 121 | 122 | The following example recognises and checks if a number is a valid signed integer or not (eg. ``-45`` or ``+45``). 123 | 124 | .. code-block:: python 125 | 126 | 127 | from edify import RegexBuilder 128 | 129 | # expression for matching any signed integer. compiles to '^[\+\-]{1}\d+$' 130 | expr = ( 131 | RegexBuilder() 132 | .start_of_input() 133 | .exactly(1).any_of() # capture either '+' or '-', exactly once 134 | .char('+') 135 | .char('-') 136 | .end() 137 | .one_or_more().digit() # capture any number of digits 138 | .end_of_input() 139 | .to_regex() 140 | ) 141 | 142 | if expr.match('-69'): 143 | print("Matched") # prints matched 144 | 145 | Building Regex Example: Simple URL Validator 146 | -------------------------------------------- 147 | 148 | The following example checks if a string is a valid url in the form of ``https://www.example.com/path/to/file.ext`` 149 | 150 | 151 | .. code-block:: python 152 | 153 | 154 | from edify import RegexBuilder 155 | 156 | # expression for validating URLs 157 | validate_urls = ( 158 | RegexBuilder() 159 | .optional().string('http://') # look for an optional "http://" 160 | .optional().string('https://') # or "https://" 161 | .one_or_more().any_of() # let x = any characters between (a-z, 0-9, '-', and '.') 162 | .range('a', 'z') # now capture one or more groups of "x" 163 | .range('0', '9') # essentially capturing all url patterns in the form of 164 | .any_of_chars('.-') # xxx.yyyyyyy.abra-cadabra.com 165 | .end() 166 | .at_least(2).any_of() # this is to make sure we get at least 2 characters in 167 | .range('a', 'z') # the end of the string, which would be our domain 168 | .end() 169 | .zero_or_more().any_of() 170 | .range('a', 'z') # same logic as capturing the url in step 1, but now 171 | .range('0', '9') # we are essentially looking for an optional path 172 | .any_of_chars('/.-_%') # and we add some more characters supported in path 173 | .end() 174 | ) 175 | 176 | # compiles to '^(?:http://)?(?:https://)?([a-z0-9\\.]+[a-z]{2,}[a-z0-9/\\.\\-_%]*)$' 177 | expr = ( 178 | RegexBuilder() 179 | .ignore_case() # case does not matter 180 | .subexpression(validate_urls) # directly writing the subexpression works too 181 | .to_regex() # convert to regex finally 182 | ) 183 | 184 | 185 | if expr.match('https://SOMETHING.www.exam-ple.com/path/to/file.txt'): 186 | print("Matched") # prints matched 187 | 188 | 189 | Building Regex Example: Simple Password Validator 190 | ------------------------------------------------- 191 | The regular expression below cheks that a password: 192 | 193 | * Has minimum 8 characters in length. 194 | * At least one uppercase English letter. 195 | * At least one lowercase English letter. 196 | * At least one digit. 197 | * At least one special character from ``#?!@$%^&*-``. 198 | 199 | .. code-block:: python 200 | 201 | from edify import RegexBuilder 202 | 203 | # expression for validating passwords - complies to '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$' 204 | expr = ( 205 | RegexBuilder() 206 | .start_of_input() # asserts position at start of a line 207 | .assert_ahead() # positive look ahead 208 | # matches any character in range 'A' to 'Z' zero and unlimited times, 209 | # as few times as possible, expanding as needed (lazy) - matching at least 1 uppercase character 210 | .zero_or_more_lazy().any_char().range('A', 'Z') 211 | .end() 212 | .assert_ahead() 213 | # at least 1 lowercase character 214 | .zero_or_more_lazy().any_char().range('a', 'z') 215 | .end() 216 | .assert_ahead() 217 | # at least 1 number 218 | .zero_or_more_lazy().any_char().range('0', '9') 219 | .end() 220 | .assert_ahead() 221 | # at least 1 special character present in the list 222 | .zero_or_more_lazy().any_char().any_of_chars('#?!@$%^&*-') 223 | .end() 224 | .at_least(8).any_char() # must be at least 8 characters long 225 | .end_of_input() 226 | .to_regex() 227 | ) 228 | 229 | if expr.match('-Secr3t!'): 230 | print("Matched") # prints matched 231 | 232 | 233 | Further Documentation 234 | --------------------- 235 | 236 | Further API documentation is available on `edify.rftd.io `_. 237 | 238 | Why Edify? 239 | =========== 240 | 241 | Regex is a powerful tool, but its syntax is not very intuitive and can be difficult to build, understand, and use. It gets even more difficult when you have to deal with backtracking, look-ahead, and other features that make regex difficult. 242 | 243 | That's where Edify becomes extremely useful. It allows you to create regular expressions in a programmatic way by invoking the ``RegexBuilder`` class [#f1]_. The API uses the `fluent builder pattern `_, and is completely immutable. It is built to be discoverable and predictable. 244 | 245 | - Properties and methods describe what they do in plain English. 246 | - Order matters! Quantifiers are specified before the thing they change, just like in English (e.g. ``RegexBuilder().exactly(5).digit()``). 247 | - If you make a mistake, you'll know how to fix it. Edify will guide you towards a fix if your expression is invalid. 248 | - ``subexpressions`` can be used to create meaningful, reusable components. 249 | 250 | Edify turns those complex and unwieldy regexes that appear in code reviews into something that can be read, understood, and **properly reviewed** by your peers - and maintained by anyone! 251 | 252 | 253 | .. _SuperExpressive: https://github.com/francisrstokes/super-expressive 254 | 255 | .. [1]: 256 | 257 | License & Contributing 258 | ====================== 259 | 260 | This project is licensed under `Apache Software License 2.0 `_. See `Contributing Guidelines `_ for information on how to contribute to this project. 261 | 262 | Contributors 263 | ------------ 264 | .. image:: https://contrib.rocks/image?repo=luciferreeves/edify 265 | 266 | 267 | .. rubric:: Footnotes 268 | 269 | .. [#f1] ``RegexBuilder`` class based on the `SuperExpressive`_ library. 270 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | from os.path import abspath 11 | from os.path import dirname 12 | from os.path import exists 13 | from os.path import join 14 | from os.path import relpath 15 | 16 | base_path = dirname(dirname(abspath(__file__))) 17 | templates_path = join(base_path, "ci", "templates") 18 | 19 | 20 | def check_call(args): 21 | print("+", *args) 22 | subprocess.check_call(args) 23 | 24 | 25 | def exec_in_env(): 26 | env_path = join(base_path, ".tox", "bootstrap") 27 | if sys.platform == "win32": 28 | bin_path = join(env_path, "Scripts") 29 | else: 30 | bin_path = join(env_path, "bin") 31 | if not exists(env_path): 32 | import subprocess 33 | 34 | print("Making bootstrap env in: {0} ...".format(env_path)) 35 | try: 36 | check_call([sys.executable, "-m", "venv", env_path]) 37 | except subprocess.CalledProcessError: 38 | try: 39 | check_call([sys.executable, "-m", "virtualenv", env_path]) 40 | except subprocess.CalledProcessError: 41 | check_call(["virtualenv", env_path]) 42 | print("Installing `jinja2` into bootstrap environment...") 43 | check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) 44 | python_executable = join(bin_path, "python") 45 | if not os.path.exists(python_executable): 46 | python_executable += '.exe' 47 | 48 | print("Re-executing with: {0}".format(python_executable)) 49 | print("+ exec", python_executable, __file__, "--no-env") 50 | os.execv(python_executable, [python_executable, __file__, "--no-env"]) 51 | 52 | 53 | def main(): 54 | import jinja2 55 | 56 | print("Project path: {0}".format(base_path)) 57 | 58 | jinja = jinja2.Environment( 59 | loader=jinja2.FileSystemLoader(templates_path), 60 | trim_blocks=True, 61 | lstrip_blocks=True, 62 | keep_trailing_newline=True, 63 | ) 64 | 65 | tox_environments = [ 66 | line.strip() 67 | # 'tox' need not be installed globally, but must be importable 68 | # by the Python that is running this script. 69 | # This uses sys.executable the same way that the call in 70 | # cookiecutter-pylibrary/hooks/post_gen_project.py 71 | # invokes this bootstrap.py itself. 72 | for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() 73 | ] 74 | tox_environments = [line for line in tox_environments if line.startswith('py')] 75 | 76 | for root, _, files in os.walk(templates_path): 77 | for name in files: 78 | relative = relpath(root, templates_path) 79 | with open(join(base_path, relative, name), "w") as fh: 80 | fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) 81 | print("Wrote {}".format(name)) 82 | print("DONE.") 83 | 84 | 85 | if __name__ == "__main__": 86 | args = sys.argv[1:] 87 | if args == ["--no-env"]: 88 | main() 89 | elif not args: 90 | exec_in_env() 91 | else: 92 | print("Unexpected arguments {0}".format(args), file=sys.stderr) 93 | sys.exit(1) 94 | -------------------------------------------------------------------------------- /ci/requirements.txt: -------------------------------------------------------------------------------- 1 | virtualenv>=16.6.0 2 | pip>=19.1.1 3 | setuptools>=18.0.1 4 | six>=1.14.0 5 | tox 6 | -------------------------------------------------------------------------------- /ci/templates/.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | image: Visual Studio 2019 4 | environment: 5 | matrix: 6 | - TOXENV: check 7 | TOXPYTHON: C:\Python36\python.exe 8 | PYTHON_HOME: C:\Python36 9 | PYTHON_VERSION: '3.6' 10 | PYTHON_ARCH: '32' 11 | {% for env in tox_environments %} 12 | {% if env.startswith(('py2', 'py3')) %} 13 | - TOXENV: {{ env }},codecov{{ "" }} 14 | TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe 15 | PYTHON_HOME: C:\Python{{ env[2:4] }} 16 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 17 | PYTHON_ARCH: '32' 18 | {% if 'nocov' in env %} 19 | WHEEL_PATH: .tox/dist 20 | {% endif %} 21 | - TOXENV: {{ env }},codecov{{ "" }} 22 | TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe 23 | PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 24 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 25 | PYTHON_ARCH: '64' 26 | {% if 'nocov' in env %} 27 | WHEEL_PATH: .tox/dist 28 | {% endif %} 29 | {% endif %}{% endfor %} 30 | init: 31 | - ps: echo $env:TOXENV 32 | - ps: ls C:\Python* 33 | install: 34 | - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' 35 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 36 | - '%PYTHON_HOME%\Scripts\pip --version' 37 | - '%PYTHON_HOME%\Scripts\tox --version' 38 | test_script: 39 | - %PYTHON_HOME%\Scripts\tox 40 | on_failure: 41 | - ps: dir "env:" 42 | - ps: get-content .tox\*\log\* 43 | 44 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 45 | # on_finish: 46 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 47 | -------------------------------------------------------------------------------- /ci/templates/.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: {{ '${{ matrix.name }}' }} 6 | runs-on: {{ '${{ matrix.os }}' }} 7 | timeout-minutes: 30 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - name: 'check' 13 | python: '3.9' 14 | toxpython: 'python3.9' 15 | tox_env: 'check' 16 | os: 'ubuntu-latest' 17 | - name: 'docs' 18 | python: '3.9' 19 | toxpython: 'python3.9' 20 | tox_env: 'docs' 21 | os: 'ubuntu-latest' 22 | {% for env in tox_environments %} 23 | {% set prefix = env.split('-')[0] -%} 24 | {% if prefix.startswith('pypy') %} 25 | {% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} 26 | {% set cpython %}pp{{ prefix[4:5] }}{% endset %} 27 | {% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} 28 | {% else %} 29 | {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} 30 | {% set cpython %}cp{{ prefix[2:] }}{% endset %} 31 | {% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} 32 | {% endif %} 33 | {% for os, python_arch in [ 34 | ['ubuntu', 'x64'], 35 | ['windows', 'x64'], 36 | ['macos', 'x64'], 37 | ] %} 38 | - name: '{{ env }} ({{ os }})' 39 | python: '{{ python }}' 40 | toxpython: '{{ toxpython }}' 41 | python_arch: '{{ python_arch }}' 42 | tox_env: '{{ env }}{% if 'cover' in env %},codecov{% endif %}' 43 | os: '{{ os }}-latest' 44 | {% endfor %} 45 | {% endfor %} 46 | steps: 47 | - uses: actions/checkout@v2 48 | with: 49 | fetch-depth: 0 50 | - uses: actions/setup-python@v2 51 | with: 52 | python-version: {{ '${{ matrix.python }}' }} 53 | architecture: {{ '${{ matrix.python_arch }}' }} 54 | - name: install dependencies 55 | run: | 56 | python -mpip install --progress-bar=off -r ci/requirements.txt 57 | virtualenv --version 58 | pip --version 59 | tox --version 60 | pip list --format=freeze 61 | - name: test 62 | env: 63 | TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' 64 | run: > 65 | tox -e {{ '${{ matrix.tox_env }}' }} -v 66 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/built-in/index.rst: -------------------------------------------------------------------------------- 1 | .. |toggleStart| raw:: html 2 | 3 |
4 | View Supported Locales 5 | 6 | .. |toggleEnd| raw:: html 7 | 8 |
9 | 10 | 11 | Pre-Built Pattern API Reference 12 | ================================ 13 | 14 | Edify allows you to verify a string quickly by providing commonly used regex patterns in its extensive set of built-in patterns. To tap into a pattern, simply import the pattern function from the ``edify.library`` module. For example, to verify that a string is a valid email address, you can use the ``email`` pattern. The pattern will return either ``True`` or ``False`` depending on whether the string matches the pattern. 15 | 16 | email(email: str) 17 | ----------------- 18 | 19 | The ``email`` function verifies that a string is a valid email address. The function takes a ``string`` argument which is supposed to be a valid email address. The function returns ``True`` if the string is a valid email address, and ``False`` otherwise. 20 | 21 | .. warning:: 22 | 23 | The ``email`` function is not a complete email address validator. It only checks that the string is in the correct format. It does not check that the domain name is valid or that the email address actually exists. This shall also be noted that there are certain trade-offs while validating email addresses using regular expressions. Regular expressions do not serve as a robust solution and should be avoided while validating complex email addresses. To learn more, go to `regular-expressions.info/email.html `_. 24 | 25 | To use the ``email`` function, import it from the ``edify.library`` module. 26 | 27 | .. code-block:: python 28 | 29 | from edify.library import email 30 | 31 | Then, call the ``email`` function with a string argument. 32 | 33 | .. code-block:: python 34 | 35 | email('hello@example.com') # returns True 36 | email('hello') # returns False 37 | 38 | email_rfc_5322(email: str) 39 | -------------------------- 40 | 41 | The ``email_rfc_5322`` function verifies that a string is a valid email address according to the `RFC 5322 `_ standard which allows for the most complete validation. Usually, you should not use it because it is an overkill. In most cases apps are not able to handle all emails that this regex allows. The function takes a ``string`` argument which is supposed to be a valid email address. The function returns ``True`` if the string is a valid email address, and ``False`` otherwise. 42 | 43 | You can use the ``email_rfc_5322`` function as follows: 44 | 45 | .. code-block:: python 46 | 47 | from edify.library import email_rfc_5322 48 | 49 | email_rfc_5322('hello@example.com') # returns True 50 | email_rfc_5322('hello') # returns False 51 | 52 | phone(phone: str) 53 | ----------------- 54 | 55 | The ``phone`` function verifies that a string is a valid phone number. The function takes a ``string`` argument which is supposed to be a valid phone number. The function returns ``True`` if the string is a valid phone number, and ``False`` otherwise. 56 | 57 | .. warning:: 58 | 59 | The ``phone`` function is not a complete phone number validator. It only checks that the string is in the correct format. It does not check that the phone number actually exists. 60 | 61 | You can use the ``phone`` function as follows: 62 | 63 | .. code-block:: python 64 | 65 | from edify.library import phone 66 | 67 | phone('1234567890') # returns True 68 | phone('123456789') # returns False 69 | phone('+1 (123) 456-7890') # returns True 70 | phone('123-456-7890') # returns True 71 | phone('9012') # returns False 72 | phone('+1 (615) 243-') # returns False 73 | 74 | 75 | ipv4(ip: str) 76 | ------------- 77 | 78 | The ``ipv4`` function verifies that a string is a valid IPv4 address. The function takes a ``string`` argument which is supposed to be a valid IPv4 address. The function returns ``True`` if the string is a valid IPv4 address, and ``False`` otherwise. 79 | 80 | You can use the ``ipv4`` function as follows: 81 | 82 | .. code-block:: python 83 | 84 | from edify.library import ipv4 85 | 86 | ipv4('128.128.128.128') # returns True 87 | ipv4('128.128.128') # returns False 88 | 89 | 90 | ipv6(ip: str) 91 | ------------- 92 | 93 | The ``ipv6`` function verifies that a string is a valid IPv6 address. The function takes a ``string`` argument which is supposed to be a valid IPv6 address. The function returns ``True`` if the string is a valid IPv6 address, and ``False`` otherwise. 94 | 95 | You can use the ``ipv6`` function as follows: 96 | 97 | .. code-block:: python 98 | 99 | from edify.library import ipv6 100 | 101 | ipv6('2001:0db8:85a3:0000:0000:8a2e:0370:7334') # returns True 102 | ipv6('2001:0db8:85a3:0000:0000:8a2e:0370') # returns False 103 | 104 | date(date: str) 105 | --------------- 106 | 107 | The ``date`` function verifies that a string is a valid date. The function takes a ``string`` argument which is supposed to be a valid date. The function returns ``True`` if the string is a valid date, and ``False`` otherwise. 108 | 109 | .. warning:: 110 | The ``date`` function validates the string against a date format (D/M/YYYY or M/D/YYYY). This however does not guarantee that the date would be valid. For example, the string ``31-02-2017`` is a valid date according to the date format, but it is not a valid date. 111 | 112 | While there are some regular expressions that allow more complex date validations, it is usually better to validate dates using special date and time libraries. For example, in Python datetime package can be used for these purposes. In this case, the validation will look like this: 113 | 114 | .. code-block:: python 115 | 116 | from datetime import datetime 117 | 118 | try: 119 | datetime.strptime('31-02-2017', '%d-%m-%Y') 120 | except ValueError: 121 | print('Invalid date') 122 | else: 123 | print('Valid date') 124 | 125 | You can use the ``date`` function as follows: 126 | 127 | .. code-block:: python 128 | 129 | from edify.library import date 130 | 131 | date('31/12/2017') # returns True 132 | date('31-12-2017') # returns False 133 | 134 | iso_date(date: str) 135 | ------------------- 136 | 137 | The ISO 8061 is an international standard for exchanging and serializing date and time data. The ``iso_date`` function verifies that a string is a valid ISO date. The function takes a ``string`` argument which is supposed to be a valid ISO date. The function returns ``True`` if the string is a valid ISO date, and ``False`` otherwise. 138 | 139 | You can use the ``iso_date`` function as follows: 140 | 141 | .. code-block:: python 142 | 143 | from edify.library import iso_date 144 | 145 | iso_date('2021-11-04T22:32:47.142354-10:00') # returns True 146 | iso_date('12/12/2022') # returns False 147 | 148 | url(url: str, match?: list) 149 | --------------------------- 150 | 151 | The ``url`` function verifies that a string is a valid URL. The function takes a ``string`` argument which is supposed to be a valid URL. The function returns ``True`` if the string is a valid URL, and ``False`` otherwise. 152 | 153 | .. warning:: 154 | 155 | The ``url`` function is not a complete URL validator. It only checks that the string is in the correct format. It does not check that the URL actually exists. 156 | 157 | You can use the ``url`` function as follows: 158 | 159 | .. code-block:: python 160 | 161 | from edify.library import url 162 | 163 | url('https://example.com') # returns True 164 | url('example.com') # returns True 165 | url('example') # returns False 166 | 167 | The ``url`` function also accepts an optional ``match`` argument. The ``match`` argument is a list of strings that you can use to configure what types of URLs the function should match. The ``match`` argument can have the following values: 168 | 169 | * ``'proto'`` - matches URLs with a protocol (e.g. ``https://example.com`` or ``http://example.com``) 170 | * ``'no_proto'`` - matches URLs without a protocol (e.g. ``example.com``) 171 | 172 | By default, the ``url`` function matches both URLs with and without a protocol. You can use the ``match`` argument to configure the function to match only URLs with a protocol or only URLs without a protocol. For example, the following code will match only URLs without a protocol: 173 | 174 | .. code-block:: python 175 | 176 | from edify.library import url 177 | 178 | url('example.com', match=['no_proto']) # returns True 179 | url('https://example.com', match=['no_proto']) # returns False 180 | 181 | If you supply an Invalid or empty value in the ``match`` list argument, the function will raise a ``ValueError`` exception. Similarly, if you supply another data type in the ``match`` list argument, the function will raise a ``TypeError`` exception. 182 | 183 | .. code-block:: python 184 | 185 | from edify.library import url 186 | 187 | url('example.com', match=['invalid']) # raises ValueError 188 | url('example.com', match=['no_proto', 'invalid']) # raises ValueError 189 | url('example.com', match=['no_proto', 1]) # raises TypeError 190 | 191 | uuid(uuid: str) 192 | --------------- 193 | 194 | The ``uuid`` function verifies that a string is a valid UUID. The function takes a ``string`` argument which is supposed to be a valid UUID. The function returns ``True`` if the string is a valid UUID, and ``False`` otherwise. 195 | 196 | You can use the ``uuid`` function as follows: 197 | 198 | .. code-block:: python 199 | 200 | from edify.library import uuid 201 | 202 | uuid('123e4567-e89b-12d3-a456-426655440000') # returns True 203 | uuid('123e4567-e') # returns False 204 | 205 | zip(zip: str, locale?: str) 206 | --------------------------- 207 | 208 | The ``zip`` function verifies that a string is a valid ZIP code. The function takes a ``string`` argument which is supposed to be a valid ZIP code. The function returns ``True`` if the string is a valid ZIP code, and ``False`` otherwise. 209 | 210 | The ``zip`` function also accepts an optional ``locale`` argument. The ``locale`` argument is a string that you can use to configure what types of ZIP codes the function should match. You can view the ``locale`` argument values below. 211 | 212 | |toggleStart| 213 | 214 | .. list-table:: 215 | :header-rows: 1 216 | 217 | * - Country 218 | - Locale 219 | 220 | * - Afghanistan 221 | - AF 222 | 223 | * - Albania 224 | - AL 225 | 226 | * - Algeria 227 | - DZ 228 | 229 | * - American Samoa 230 | - AS 231 | 232 | * - Andorra 233 | - AD 234 | 235 | * - Angola 236 | - AO 237 | 238 | * - Anguilla 239 | - AI 240 | 241 | * - Antigua and Barbuda 242 | - AG 243 | 244 | * - Argentina 245 | - AR 246 | 247 | * - Armenia 248 | - AM 249 | 250 | * - Aruba 251 | - AW 252 | 253 | * - Australia 254 | - AU 255 | 256 | * - Austria 257 | - AT 258 | 259 | * - Azerbaijan 260 | - AZ 261 | 262 | * - Bahamas 263 | - BS 264 | 265 | * - Bahrain 266 | - BH 267 | 268 | * - Bangladesh 269 | - BD 270 | 271 | * - Barbados 272 | - BB 273 | 274 | * - Belarus 275 | - BY 276 | 277 | * - Belgium 278 | - BE 279 | 280 | * - Belize 281 | - BZ 282 | 283 | * - Benin 284 | - BJ 285 | 286 | * - Bermuda 287 | - BM 288 | 289 | * - Bhutan 290 | - BT 291 | 292 | * - Bolivia 293 | - BO 294 | 295 | * - Bonaire 296 | - BQ 297 | 298 | * - Bosnia and Herzegovina 299 | - BA 300 | 301 | * - Botswana 302 | - BW 303 | 304 | * - Brazil 305 | - BR 306 | 307 | * - Brunei 308 | - BN 309 | 310 | * - Bulgaria 311 | - BG 312 | 313 | * - Burkina Faso 314 | - BF 315 | 316 | * - Burundi 317 | - BI 318 | 319 | * - Cambodia 320 | - KH 321 | 322 | * - Cameroon 323 | - CM 324 | 325 | * - Canada 326 | - CA 327 | 328 | * - Canary Islands 329 | - CI 330 | 331 | * - Cape Verde 332 | - CV 333 | 334 | * - Cayman Islands 335 | - KY 336 | 337 | * - Central African Republic 338 | - CF 339 | 340 | * - Chad 341 | - TD 342 | 343 | * - Channel Islands 344 | - CI 345 | 346 | * - Chile 347 | - CL 348 | 349 | * - China, People's Republic 350 | - CN 351 | 352 | * - Colombia 353 | - CO 354 | 355 | * - Comoros 356 | - KM 357 | 358 | * - Congo 359 | - CG 360 | 361 | * - Congo, The Democratic Republic of 362 | - CD 363 | 364 | * - Cook Islands 365 | - CK 366 | 367 | * - Costa Rica 368 | - CR 369 | 370 | * - Côte d'Ivoire 371 | - CI 372 | 373 | * - Croatia 374 | - HR 375 | 376 | * - Cuba 377 | - CU 378 | 379 | * - Curacao 380 | - CW 381 | 382 | * - Cyprus 383 | - CY 384 | 385 | * - Czech Republic 386 | - CZ 387 | 388 | * - Denmark 389 | - DK 390 | 391 | * - Djibouti 392 | - DJ 393 | 394 | * - Dominica 395 | - DM 396 | 397 | * - Dominican Republic 398 | - DO 399 | 400 | * - East Timor 401 | - TL 402 | 403 | * - Ecuador 404 | - EC 405 | 406 | * - Egypt 407 | - EG 408 | 409 | * - El Salvador 410 | - SV 411 | 412 | * - Eritrea 413 | - ER 414 | 415 | * - Estonia 416 | - EE 417 | 418 | * - Ethiopia 419 | - ET 420 | 421 | * - Falkland Islands 422 | - FK 423 | 424 | * - Faroe Islands 425 | - FO 426 | 427 | * - Fiji 428 | - FJ 429 | 430 | * - Finland 431 | - FI 432 | 433 | * - France 434 | - FR 435 | 436 | * - French Polynesia 437 | - PF 438 | 439 | * - Gabon 440 | - GA 441 | 442 | * - Gambia 443 | - GM 444 | 445 | * - Georgia 446 | - GE 447 | 448 | * - Germany 449 | - DE 450 | 451 | * - Ghana 452 | - GH 453 | 454 | * - Gibraltar 455 | - GI 456 | 457 | * - Greece 458 | - GR 459 | 460 | * - Greenland 461 | - GL 462 | 463 | * - Grenada 464 | - GD 465 | 466 | * - Guadeloupe 467 | - GP 468 | 469 | * - Guam 470 | - GU 471 | 472 | * - Guatemala 473 | - GT 474 | 475 | * - Guernsey 476 | - GG 477 | 478 | * - Guinea-Bissau 479 | - GW 480 | 481 | * - Guinea-Equatorial 482 | - GQ 483 | 484 | * - Guinea Republic 485 | - GN 486 | 487 | * - Guyana (British) 488 | - GY 489 | 490 | * - Guyana (French) 491 | - GF 492 | 493 | * - Haiti 494 | - HT 495 | 496 | * - Honduras 497 | - HN 498 | 499 | * - Hong Kong 500 | - HK 501 | 502 | * - Hungary 503 | - HU 504 | 505 | * - Iceland 506 | - IS 507 | 508 | * - India 509 | - IN 510 | 511 | * - Indonesia 512 | - ID 513 | 514 | * - Iran 515 | - IR 516 | 517 | * - Iraq 518 | - IQ 519 | 520 | * - Ireland, Republic of 521 | - IE 522 | 523 | * - Islas Malvinas 524 | - FK 525 | 526 | * - Israel 527 | - IL 528 | 529 | * - Italy 530 | - IT 531 | 532 | * - Ivory Coast 533 | - CI 534 | 535 | * - Jamaica 536 | - JM 537 | 538 | * - Japan 539 | - JP 540 | 541 | * - Jersey 542 | - JE 543 | 544 | * - Jordan 545 | - JO 546 | 547 | * - Kazakhstan 548 | - KZ 549 | 550 | * - Kenya 551 | - KE 552 | 553 | * - Kiribati 554 | - KI 555 | 556 | * - Korea, Republic of 557 | - KR 558 | 559 | * - Korea, The D.P.R of 560 | - KP 561 | 562 | * - Kosovo 563 | - XK 564 | 565 | * - Kuwait 566 | - KW 567 | 568 | * - Kyrgyzstan 569 | - KG 570 | 571 | * - Laos 572 | - LA 573 | 574 | * - Latvia 575 | - LV 576 | 577 | * - Lebanon 578 | - LB 579 | 580 | * - Lesotho 581 | - LS 582 | 583 | * - Liberia 584 | - LR 585 | 586 | * - Libya 587 | - LY 588 | 589 | * - Liechtenstein 590 | - LI 591 | 592 | * - Lithuania 593 | - LT 594 | 595 | * - Luxembourg 596 | - LU 597 | 598 | * - Macau 599 | - MO 600 | 601 | * - Macedonia, Republic of 602 | - MK 603 | 604 | * - Madagascar 605 | - MG 606 | 607 | * - Malawi 608 | - MW 609 | 610 | * - Malaysia 611 | - MY 612 | 613 | * - Maldives 614 | - MV 615 | 616 | * - Mali 617 | - ML 618 | 619 | * - Malta 620 | - MT 621 | 622 | * - Marshall Islands 623 | - MH 624 | 625 | * - Martinique 626 | - MQ 627 | 628 | * - Mauritania 629 | - MR 630 | 631 | * - Mauritius 632 | - MU 633 | 634 | * - Mayotte 635 | - YT 636 | 637 | * - Mexico 638 | - MX 639 | 640 | * - Moldova, Republic of 641 | - MD 642 | 643 | * - Monaco 644 | - MC 645 | 646 | * - Mongolia 647 | - MN 648 | 649 | * - Montenegro 650 | - ME 651 | 652 | * - Montserrat 653 | - MS 654 | 655 | * - Morocco 656 | - MA 657 | 658 | * - Mozambique 659 | - MZ 660 | 661 | * - Myanmar 662 | - MM 663 | 664 | * - Namibia 665 | - NA 666 | 667 | * - Nauru 668 | - NR 669 | 670 | * - Nepal 671 | - NP 672 | 673 | * - Netherlands 674 | - NL 675 | 676 | * - New Caledonia 677 | - NC 678 | 679 | * - New Zealand 680 | - NZ 681 | 682 | * - Nicaragua 683 | - NI 684 | 685 | * - Niger 686 | - NE 687 | 688 | * - Nigeria 689 | - NG 690 | 691 | * - Niue 692 | - NU 693 | 694 | * - Northern Mariana Islands 695 | - MP 696 | 697 | * - Norway 698 | - NO 699 | 700 | * - Oman 701 | - OM 702 | 703 | * - Pakistan 704 | - PK 705 | 706 | * - Palau 707 | - PW 708 | 709 | * - Panama 710 | - PA 711 | 712 | * - Papua New Guinea 713 | - PG 714 | 715 | * - Paraguay 716 | - PY 717 | 718 | * - Peru 719 | - PE 720 | 721 | * - Philippines 722 | - PH 723 | 724 | * - Poland 725 | - PL 726 | 727 | * - Portugal 728 | - PT 729 | 730 | * - Puerto Rico 731 | - PR 732 | 733 | * - Qatar 734 | - QA 735 | 736 | * - Réunion 737 | - RE 738 | 739 | * - Romania 740 | - RO 741 | 742 | * - Russian Federation 743 | - RU 744 | 745 | * - Rwanda 746 | - RW 747 | 748 | * - Saipan 749 | - MP 750 | 751 | * - Samoa 752 | - WS 753 | 754 | * - Sao Tome and Principe 755 | - ST 756 | 757 | * - Saudi Arabia 758 | - SA 759 | 760 | * - Senegal 761 | - SN 762 | 763 | * - Serbia 764 | - RS 765 | 766 | * - Seychelles 767 | - SC 768 | 769 | * - Sierra Leone 770 | - SL 771 | 772 | * - Singapore 773 | - SG 774 | 775 | * - Slovakia 776 | - SK 777 | 778 | * - Slovenia 779 | - SI 780 | 781 | * - Solomon Islands 782 | - SB 783 | 784 | * - Somalia 785 | - SO 786 | 787 | * - South Africa 788 | - ZA 789 | 790 | * - South Sudan 791 | - SS 792 | 793 | * - Spain 794 | - ES 795 | 796 | * - Sri Lanka 797 | - LK 798 | 799 | * - St. Barthélemy 800 | - BL 801 | 802 | * - St. Croix 803 | - VI 804 | 805 | * - St. Eustatius 806 | - SE 807 | 808 | * - St. Helena 809 | - SH 810 | 811 | * - St. John 812 | - AG 813 | 814 | * - St. Kitts and Nevis 815 | - KN 816 | 817 | * - St. Lucia 818 | - LC 819 | 820 | * - St. Maarten 821 | - SX 822 | 823 | * - St. Thomas 824 | - VI 825 | 826 | * - St. Vincent and the Grenadines 827 | - VC 828 | 829 | * - Sudan 830 | - SD 831 | 832 | * - Suriname 833 | - SR 834 | 835 | * - Swaziland 836 | - SZ 837 | 838 | * - Sweden 839 | - SE 840 | 841 | * - Switzerland 842 | - CH 843 | 844 | * - Syria 845 | - SY 846 | 847 | * - Tahiti 848 | - PF 849 | 850 | * - Taiwan 851 | - TW 852 | 853 | * - Tanzania 854 | - TZ 855 | 856 | * - Thailand 857 | - TH 858 | 859 | * - Togo 860 | - TG 861 | 862 | * - Tonga 863 | - TO 864 | 865 | * - Tortola 866 | - VG 867 | 868 | * - Trinidad and Tobago 869 | - TT 870 | 871 | * - Tunisia 872 | - TN 873 | 874 | * - Turkey 875 | - TR 876 | 877 | * - Turkmenistan 878 | - TM 879 | 880 | * - Turks and Caicos Islands 881 | - TC 882 | 883 | * - Tuvalu 884 | - TV 885 | 886 | * - Uganda 887 | - UG 888 | 889 | * - Ukraine 890 | - UA 891 | 892 | * - United Arab Emirates 893 | - AE 894 | 895 | * - United Kingdom 896 | - GB 897 | 898 | * - United States of America 899 | - US 900 | 901 | * - Uruguay 902 | - UY 903 | 904 | * - Uzbekistan 905 | - UZ 906 | 907 | * - Vanuatu 908 | - VU 909 | 910 | * - Venezuela 911 | - VE 912 | 913 | * - Vietnam 914 | - VN 915 | 916 | * - Virgin Islands (British) 917 | - VG 918 | 919 | * - Virgin Islands (US) 920 | - VI 921 | 922 | * - Yemen 923 | - YE 924 | 925 | * - Zambia 926 | - ZM 927 | 928 | * - Zimbabwe 929 | - ZW 930 | 931 | |toggleEnd| 932 | 933 | By default, the ``zip`` function matches ZIP codes for "US". Here's an example of how to use the ``zip`` function to match ZIP codes: 934 | 935 | .. code-block:: python 936 | 937 | from edify.library import zip 938 | 939 | zip('12345') # returns True 940 | zip('1234') # returns False 941 | zip('12345', locale='US') # returns True 942 | zip('12345-1234') # returns True 943 | zip('12345-1234', locale='US') # returns True 944 | zip('123456', locale='IN') # returns True 945 | 946 | If you supply an Invalid or empty value in the ``locale`` argument, the function will raise a ``ValueError`` exception. Similarly, if you supply another data type in the ``locale`` argument, the function will raise a ``TypeError`` exception. 947 | 948 | 949 | guid(guid: str) 950 | --------------- 951 | 952 | The ``guid`` function validates a GUID (Globally Unique Identifier) string. The function returns ``True`` if the string is a valid GUID, and ``False`` otherwise. 953 | 954 | Here's an example of how to use the ``guid`` function: 955 | 956 | .. code-block:: python 957 | 958 | from edify.library import guid 959 | 960 | guid('6ba7b810-9dad-11d1-80b4-00c04fd430c8') # returns True 961 | guid('{51d52cf1-83c9-4f02-b117-703ecb728b74}') # returns True 962 | guid('{51d52cf1-83c9-4f02-b117-703ecb728-b74}') # returns False 963 | 964 | password(password: str, min_length?: int, max_length?: int, min_upper?: int, min_lower?: int, min_digit?: int, min_special?: int, special_chars?: str) 965 | ------------------------------------------------------------------------------------------------------------------------------------------------------------ 966 | 967 | The ``password`` function validates a password string. The function returns ``True`` if the string is a valid password, and ``False`` otherwise. 968 | 969 | The ``password`` function takes the following arguments: 970 | 971 | * ``password``: The password string to validate. 972 | * ``min_length``: The minimum length of the password. The default value is 8. 973 | * ``max_length``: The maximum length of the password. The default value is 64. 974 | * ``min_upper``: The minimum number of uppercase characters in the password. The default value is 1. 975 | * ``min_lower``: The minimum number of lowercase characters in the password. The default value is 1. 976 | * ``min_digit``: The minimum number of digits in the password. The default value is 1. 977 | * ``min_special``: The minimum number of special characters in the password. The default value is 1. 978 | * ``special_chars``: The special characters to use in the password. The default value is ``!@#$%^&*()_+-=[]{}|;':\",./<>?``. 979 | 980 | Here's an example of how to use the ``password`` function: 981 | 982 | .. code-block:: python 983 | 984 | from edify.library import password 985 | 986 | password('password') # returns False 987 | password("Password123!") # returns True 988 | password("Password123!", max_length=8) # returns False 989 | password("Password123!", min_upper=2) # returns False 990 | password("password", min_upper=0, min_digit=0, min_special=0) # returns True 991 | password("pass@#1", min_special=1, special_chars="!", min_digit=0, min_upper=0, min_length=4) # returns False 992 | 993 | ssn(ssn: str) 994 | ------------- 995 | 996 | The ``ssn`` function validates a Social Security Number (SSN) string. The function returns ``True`` if the string is a valid SSN, and ``False`` otherwise. 997 | 998 | Here's an example of how to use the ``ssn`` function: 999 | 1000 | .. code-block:: python 1001 | 1002 | from edify.library import ssn 1003 | 1004 | ssn('123-45-6789') # returns True 1005 | ssn('123-45-678') # returns False 1006 | ssn('123-45-67890') # returns False 1007 | 1008 | 1009 | mac(mac: str) 1010 | ------------- 1011 | 1012 | The ``mac`` function validates a MAC address (IEEE 802) string. The function returns ``True`` if the string is a valid MAC address, and ``False`` otherwise. 1013 | 1014 | Here's an example of how to use the ``mac`` function: 1015 | 1016 | .. code-block:: python 1017 | 1018 | from edify.library import mac 1019 | 1020 | mac('00:00:00:00:00:00') # returns True 1021 | mac('00:00:00:00:00:0') # returns False 1022 | mac('00:00:00:00:00:000') # returns False 1023 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | extensions = [ 7 | 'sphinx.ext.autodoc', 8 | 'sphinx.ext.autosummary', 9 | 'sphinx.ext.coverage', 10 | 'sphinx.ext.doctest', 11 | 'sphinx.ext.extlinks', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.napoleon', 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.viewcode', 16 | ] 17 | source_suffix = '.rst' 18 | master_doc = 'index' 19 | project = 'Edify' 20 | year = '2022' 21 | author = 'Bobby' 22 | copyright = '{0}, {1}'.format(year, author) 23 | version = release = '0.2.2' 24 | 25 | pygments_style = 'trac' 26 | templates_path = ['.'] 27 | extlinks = { 28 | 'issue': ('https://github.com/luciferreeves/edify/issues/%s', '#'), 29 | 'pr': ('https://github.com/luciferreeves/edify/pull/%s', 'PR #'), 30 | } 31 | # on_rtd is whether we are on readthedocs.org 32 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 33 | 34 | if not on_rtd: # only set the theme if we're building docs locally 35 | html_theme = 'sphinx_rtd_theme' 36 | 37 | html_use_smartypants = True 38 | html_last_updated_fmt = '%b %d, %Y' 39 | html_split_index = False 40 | html_sidebars = { 41 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 42 | } 43 | html_short_title = '%s-%s' % (project, version) 44 | 45 | napoleon_use_ivar = True 46 | napoleon_use_rtype = False 47 | napoleon_use_param = False 48 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | readme 9 | built-in/index 10 | regex-builder/index 11 | contributing 12 | authors 13 | changelog 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/regex-builder/builder/index.rst: -------------------------------------------------------------------------------- 1 | RegexBuilder 2 | ============ 3 | 4 | RegexBuilder is a class that helps you build regular expressions. It is based on the `SuperExpressive `_ library. The API uses the `fluent builder pattern `_, and is completely immutable. It is built to be discoverable and predictable. 5 | 6 | - Properties and methods describe what they do in plain English. 7 | - Order matters! Quantifiers are specified before the thing they change, just like in English (e.g. ``RegexBuilder().exactly(5).digit()``.) 8 | - If you make a mistake, you'll know how to fix it. Edify will guide you towards a fix if your expression is invalid. 9 | - ``subexpressions`` can be used to create meaningful, reusable components. 10 | 11 | .any_char() 12 | ----------- 13 | 14 | ``.any_char()`` matches any single character. 15 | 16 | .. code-block:: python 17 | 18 | from edify import RegexBuilder 19 | 20 | # returns re.compile('.') 21 | expr = RegexBuilder().any_char().to_regex() 22 | assert expr.match('a') # Matches 23 | assert expr.match('hello') # Matches 24 | 25 | 26 | .whitespace_char() 27 | ------------------ 28 | 29 | ``.whitespace_char()`` matches any whitespace character, including the special whitespace characters: ``\r\n\t\f\v``. 30 | 31 | .. code-block:: python 32 | 33 | from edify import RegexBuilder 34 | 35 | # returns re.compile('\s') 36 | expr = RegexBuilder().whitespace_char().to_regex() 37 | assert expr.match(' ') # Matches 38 | assert expr.match('\n') # Matches 39 | assert expr.match('\t') # Matches 40 | assert expr.match('\r') # Matches 41 | assert expr.match('\f') # Matches 42 | assert expr.match('\v') # Matches 43 | assert not expr.match('a') # Doesn't match 44 | assert not expr.match('hello') # Doesn't match 45 | 46 | 47 | .non_whitespace_char() 48 | ---------------------- 49 | 50 | ``.non_whitespace_char()`` matches any non-whitespace character, excluding also the special whitespace characters: ``\r\n\t\f\v``. 51 | 52 | .. code-block:: python 53 | 54 | from edify import RegexBuilder 55 | 56 | # returns re.compile('\S') 57 | expr = RegexBuilder().non_whitespace_char().to_regex() 58 | assert expr.match('a') # Matches 59 | assert expr.match('hello') # Matches 60 | assert not expr.match(' ') # Doesn't match 61 | assert not expr.match('\n') # Doesn't match 62 | assert not expr.match('\t') # Doesn't match 63 | assert not expr.match('\r') # Doesn't match 64 | assert not expr.match('\f') # Doesn't match 65 | assert not expr.match('\v') # Doesn't match 66 | assert not expr.match('\u00a0') # Doesn't match 67 | assert not expr.match('\u2000') # Doesn't match 68 | 69 | 70 | .digit() 71 | -------- 72 | 73 | ``.digit()`` matches any digit from ``0-9``. 74 | 75 | .. code-block:: python 76 | 77 | from edify import RegexBuilder 78 | 79 | # returns re.compile('\d') 80 | expr = RegexBuilder().digit().to_regex() 81 | assert expr.match('1') # Matches 82 | assert expr.match('9') # Matches 83 | assert not expr.match('a') # Doesn't match 84 | assert not expr.match('\u00a0') # Doesn't match 85 | 86 | 87 | .non_digit() 88 | ------------- 89 | 90 | ``.non_digit()`` matches any non-digit. 91 | 92 | .. code-block:: python 93 | 94 | from edify import RegexBuilder 95 | 96 | # returns re.compile('\D') 97 | expr = RegexBuilder().non_digit().to_regex() 98 | assert expr.match('a') # Matches 99 | assert expr.match('\u00a0') # Matches 100 | assert not expr.match('1') # Doesn't match 101 | assert not expr.match('9') # Doesn't match 102 | 103 | .. _word: 104 | 105 | .word() 106 | ------- 107 | 108 | 109 | ``.word()`` matches any alpha-numeric ``(a-z, A-Z, 0-9)`` characters, as well as ``_``. 110 | 111 | .. code-block:: python 112 | 113 | from edify import RegexBuilder 114 | 115 | # returns re.compile('\w') 116 | expr = RegexBuilder().word().to_regex() 117 | assert expr.match('a') # Matches 118 | assert expr.match('1') # Matches 119 | assert expr.match('_') # Matches 120 | assert expr.match('hello') # Matches 121 | 122 | 123 | .non_word() 124 | ----------- 125 | 126 | ``.non_word()`` matches any non-alpha-numeric ``(a-z, A-Z, 0-9)`` characters, excluding ``_`` as well. 127 | 128 | .. code-block:: python 129 | 130 | from edify import RegexBuilder 131 | 132 | # returns re.compile('\W') 133 | expr = RegexBuilder().non_word().to_regex() 134 | assert not expr.match('a') # Doesn't match 135 | assert not expr.match('1') # Doesn't match 136 | assert expr.match('\u00a0') # Matches 137 | assert expr.match('\u2000') # Matches 138 | assert not expr.match('_') # Doesn't match 139 | assert not expr.match('hello') # Doesn't match 140 | 141 | 142 | .word_boundary() 143 | ----------------- 144 | 145 | ``.word_boundary()`` matches (without consuming any characters) immediately between a character matched by :ref:`word` and a character not matched by :ref:`word` (in either order). 146 | 147 | .. code-block:: python 148 | 149 | from edify import RegexBuilder 150 | 151 | # returns re.compile('\d\b') 152 | expr = RegexBuilder().digit().word_boundary().to_regex() 153 | 154 | 155 | .non_word_boundary() 156 | -------------------- 157 | 158 | ``.non_word_boundary()`` matches (without consuming any characters) at the position between two characters matched by :ref:`word`. 159 | 160 | .. code-block:: python 161 | 162 | 163 | from edify import RegexBuilder 164 | 165 | # returns re.compile('\d\B') 166 | expr = RegexBuilder().digit().non_word_boundary().to_regex() 167 | 168 | .new_line() 169 | ----------- 170 | 171 | ``.new_line()`` matches the newline ``\n`` character. 172 | 173 | .. code-block:: python 174 | 175 | from edify import RegexBuilder 176 | 177 | # returns re.compile('\n') 178 | expr = RegexBuilder().new_line().to_regex() 179 | assert expr.match('\n') # Matches 180 | assert not expr.match('a') # Doesn't match 181 | assert not expr.match('hello') # Doesn't match 182 | 183 | .carriage_return() 184 | ------------------- 185 | 186 | ``.carriage_return()`` matches the carriage return ``\r`` character. 187 | 188 | .. code-block:: python 189 | 190 | from edify import RegexBuilder 191 | 192 | # returns re.compile('\r') 193 | expr = RegexBuilder().carriage_return().to_regex() 194 | assert expr.match('\r') # Matches 195 | assert not expr.match('a') # Doesn't match 196 | assert not expr.match('hello') # Doesn't match 197 | 198 | 199 | .tab() 200 | ------ 201 | 202 | ``.tab()`` matches the tab ``\t`` character. 203 | 204 | .. code-block:: python 205 | 206 | from edify import RegexBuilder 207 | 208 | # returns re.compile('\t') 209 | expr = RegexBuilder().tab().to_regex() 210 | assert expr.match('\t') # Matches 211 | assert not expr.match('a') # Doesn't match 212 | assert not expr.match('hello') # Doesn't match 213 | 214 | 215 | .null_byte() 216 | ------------ 217 | 218 | ``.null_byte()`` matches the null byte ``\0`` character. 219 | 220 | .. code-block:: python 221 | 222 | from edify import RegexBuilder 223 | 224 | # returns re.compile('\0') 225 | expr = RegexBuilder().null_byte().to_regex() 226 | assert expr.match('\0') # Matches 227 | assert not expr.match('a') # Doesn't match 228 | assert not expr.match('hello') # Doesn't match 229 | 230 | .. _any_of: 231 | 232 | .any_of() 233 | --------- 234 | 235 | ``.any_of()`` matches a choice between specified elements. Needs to be finalised with :ref:`end`. 236 | 237 | .. code-block:: python 238 | 239 | from edify import RegexBuilder 240 | 241 | # returns re.compile('(?:hello|[a-f0-9])') 242 | expr = ( 243 | RegexBuilder() 244 | .any_of() 245 | .range('a', 'f') 246 | .range('0', '9') 247 | .string('hello') 248 | .end() 249 | .to_regex() 250 | ) 251 | assert expr.match('a') # Matches 252 | assert expr.match('f') # Matches 253 | assert expr.match('9') # Matches 254 | assert expr.match('hello') # Matches 255 | assert not expr.match('g') # Doesn't match 256 | assert not expr.match('good world') # Doesn't match 257 | 258 | .. _capture: 259 | 260 | .capture() 261 | ----------- 262 | 263 | ``.capture()`` creates a capture group for the proceeding elements. Needs to be finalised with :ref:`end`. Can be later referenced with :ref:`backreference`. 264 | 265 | .. code-block:: python 266 | 267 | from edify import RegexBuilder 268 | 269 | # returns re.compile('([a-f][0-9]hello)') 270 | expr = ( 271 | RegexBuilder() 272 | .capture() 273 | .range('a', 'f') 274 | .range('0', '9') 275 | .string('hello') 276 | .end() 277 | .to_regex() 278 | ) 279 | assert expr.match('a9hello') # Matches 280 | assert expr.match('f0hello') # Matches 281 | assert not expr.match('g9hello') # Doesn't match 282 | 283 | .. _named_capture: 284 | 285 | .named_capture(name) 286 | -------------------- 287 | 288 | ``.named_capture()`` creates a named capture group for the proceeding elements. Needs to be finalised with :ref:`end`. Can be later referenced with :ref:`named_back_reference` or :ref:`backreference`. 289 | 290 | .. code-block:: python 291 | 292 | from edify import RegexBuilder 293 | 294 | # returns re.compile('(?P[a-f][0-9]hello)') 295 | expr = ( 296 | RegexBuilder() 297 | .named_capture('interestingStuff') 298 | .range('a', 'f') 299 | .range('0', '9') 300 | .string('hello') 301 | .end() 302 | .to_regex() 303 | ) 304 | assert expr.match('a9hello') # Matches 305 | assert expr.match('f0hello') # Matches 306 | assert not expr.match('g9hello') # Doesn't match 307 | 308 | .. _named_back_reference: 309 | 310 | .named_back_reference(name) 311 | --------------------------- 312 | 313 | ``.named_back_reference()`` matches exactly what was previously matched by a :ref:`named_capture`. 314 | 315 | .. warning:: 316 | 317 | Python does not support named back references. If you try to call the ``to_regex()`` method on a named back reference, it will raise an exception. For, those reasons, ``to_regex_string()`` is provided instead. It returns a string that can be used to create a regular expression. You can try using the regular expression directly with another library like `regex `_. 318 | 319 | .. code-block:: python 320 | 321 | from edify import RegexBuilder 322 | 323 | # returns /(?[a-f][0-9]hello)something else\k/ 324 | expr = ( 325 | RegexBuilder() 326 | .named_capture('interestingStuff') 327 | .range('a', 'f') 328 | .range('0', '9') 329 | .string('hello') 330 | .end() 331 | .string('something else') 332 | .named_back_reference('interestingStuff') 333 | .to_regex_string() 334 | ) 335 | 336 | .. _backreference: 337 | 338 | .back_reference(index) 339 | ---------------------- 340 | 341 | ``.back_reference()`` matches exactly what was previously matched by a :ref:`capture` or :ref:`named_capture` using a positional index. Note regex indexes start at 1, so the first capture group has index 1. 342 | 343 | .. code-block:: python 344 | 345 | from edify import RegexBuilder 346 | 347 | # returns re.compile('([a-f][0-9]hello)\\1') 348 | expr = ( 349 | RegexBuilder() 350 | .capture() 351 | .range('a', 'f') 352 | .range('0', '9') 353 | .string('hello') 354 | .end() 355 | .back_reference(1) 356 | .to_regex() 357 | ) 358 | assert expr.match('a9helloa9hello') # Matches 359 | assert not expr.match('a9helloa9hell') # Doesn't match 360 | 361 | .. _group: 362 | 363 | .group() 364 | -------- 365 | 366 | ``.group()`` creates a non-capturing group for the proceeding elements. Needs to be finalised with :ref:`end`. 367 | 368 | .. code-block:: python 369 | 370 | from edify import RegexBuilder 371 | 372 | # returns re.compile('(?:[a-f][0-9]hello)?') 373 | expr = ( 374 | RegexBuilder() 375 | .optional().group() 376 | .range('a', 'f') 377 | .range('0', '9') 378 | .string('hello') 379 | .end() 380 | .to_regex() 381 | ) 382 | assert expr.match('a9hello') # Matches 383 | assert expr.match('') # Matches 384 | assert not expr.match('g9hello') # Matches 385 | 386 | .. _end: 387 | 388 | .end() 389 | ------ 390 | 391 | ``.end()`` signifies the end of a ``RegexBuilder`` grouping, such as :ref:`capture`, :ref:`group` or :ref:`any_of` element. 392 | 393 | .. code-block:: python 394 | 395 | from edify import RegexBuilder 396 | 397 | # returns re.compile('((?:hello|[a-f0-9]))') 398 | expr = ( 399 | RegexBuilder() 400 | .capture() 401 | .any_of() 402 | .range('a', 'f') 403 | .range('0', '9') 404 | .string('hello') 405 | .end() 406 | .end() 407 | .to_regex() 408 | ) 409 | 410 | .. _assert_ahead: 411 | 412 | .assert_ahead() 413 | --------------- 414 | 415 | ``.assert_ahead()`` asserts that the proceeding elements are found without consuming them. Needs to be finalised with :ref:`end`. 416 | 417 | .. code-block:: python 418 | 419 | from edify import RegexBuilder 420 | 421 | # returns re.compile('(?=[a-f])[a-z]') 422 | expr = ( 423 | RegexBuilder() 424 | .assert_ahead() 425 | .range('a', 'f') 426 | .end() 427 | .range('a', 'z') 428 | .to_regex() 429 | ) 430 | assert expr.match('a') # Matches 431 | assert expr.match('f') # Matches 432 | assert not expr.match('g') # Doesn't match 433 | 434 | .. _assert_not_ahead: 435 | 436 | .assert_not_ahead() 437 | ------------------- 438 | 439 | ``.assert_not_ahead()`` asserts that the proceeding elements are not found without consuming them. Needs to be finalised with :ref:`end`. 440 | 441 | .. code-block:: python 442 | 443 | from edify import RegexBuilder 444 | 445 | # returns re.compile('(?![a-f])[g-z]') 446 | expr = ( 447 | RegexBuilder() 448 | .assert_not_ahead() 449 | .range('a', 'f') 450 | .end() 451 | .range('g', 'z') 452 | .to_regex() 453 | ) 454 | assert expr.match('g') # Matches 455 | assert expr.match('z') # Matches 456 | assert not expr.match('a') # Doesn't match 457 | 458 | .. _assert_behind: 459 | 460 | .assert_behind() 461 | ---------------- 462 | 463 | ``.assert_behind()`` asserts that the elements contained within are found immediately before this point in the string. Needs to be finalised with :ref:`end`. 464 | 465 | .. code-block:: python 466 | 467 | from edify import RegexBuilder 468 | 469 | # returns re.compile('(?<=hello )world') 470 | expr = ( 471 | RegexBuilder() 472 | .assert_behind() 473 | .string('hello ') 474 | .end() 475 | .string('world') 476 | .to_regex() 477 | ) 478 | 479 | .. _assert_not_behind: 480 | 481 | .assert_not_behind() 482 | -------------------- 483 | 484 | ``.assert_not_behind()`` asserts that the elements contained within are not found immediately before this point in the string. Needs to be finalised with :ref:`end`. 485 | 486 | .. code-block:: python 487 | 488 | from edify import RegexBuilder 489 | 490 | # returns re.compile('(?`_ package. To get started, import the ``RegexBuilder`` class:: 13 | 14 | from edify import RegexBuilder 15 | 16 | 17 | ASCII Only Matching 18 | -------------------- 19 | 20 | Make ``\w``, ``\W``, ``\b``, ``\B``, ``\d``, ``\D``, ``\s``, and ``\S`` perform ASCII-only matching instead of full Unicode matching. This is only meaningful for Unicode patterns, and is ignored for byte patterns. Corresponds to the inline flag ``(?a)``. 21 | 22 | Example 23 | ^^^^^^^ 24 | 25 | .. code-block:: python 26 | 27 | # returns re.compile('hello', re.ASCII) 28 | expr = RegexBuilder().ascii_only().string('hello').to_regex() 29 | 30 | Display Debug Information 31 | ------------------------- 32 | Display debug information about compiled expression. No corresponding inline flag. 33 | 34 | Example 35 | ^^^^^^^ 36 | 37 | .. code-block:: python 38 | 39 | # returns re.compile('hello', re.DEBUG) 40 | expr = RegexBuilder().debug().string('hello').to_regex() 41 | 42 | 43 | Ignore Case 44 | ------------ 45 | Perform case-insensitive matching; expressions like ``[A-Z]`` will also match lowercase letters. Full Unicode matching (such as ``Ü`` matching ``ü``) also works unless the ``re.ASCII`` flag is used to disable non-ASCII matches. Corresponds to the inline flag ``(?i)``. 46 | 47 | Example 48 | ^^^^^^^ 49 | 50 | .. code-block:: python 51 | 52 | # returns re.compile('hello', re.IGNORECASE) 53 | expr = RegexBuilder().ignore_case().string('hello').to_regex() 54 | 55 | 56 | Multi Line 57 | ---------- 58 | When specified, the pattern character ``'^'`` matches at the beginning of the string and at the beginning of each line (immediately following each newline); and the pattern character ``'$'`` matches at the end of the string and at the end of each line (immediately preceding each newline). By default, ``'^'`` matches only at the beginning of the string, and ``'$'`` only at the end of the string and immediately before the newline (if any) at the end of the string. Corresponds to the inline flag ``(?m)``. 59 | 60 | 61 | Example 62 | ^^^^^^^ 63 | 64 | .. code-block:: python 65 | 66 | # returns re.compile('hello', re.MULTILINE) 67 | expr = RegexBuilder().multi_line().string('hello').to_regex() 68 | 69 | 70 | Dot All 71 | ------- 72 | 73 | Make the ``'.'`` special character match any character at all, including a newline; without this flag, ``'.'`` will match anything *except* a newline. Corresponds to the inline flag ``(?s)``. 74 | 75 | 76 | Example 77 | ^^^^^^^ 78 | 79 | .. code-block:: python 80 | 81 | # returns re.compile('hello', re.DOTALL) 82 | expr = RegexBuilder().dot_all().string('hello').to_regex() 83 | 84 | 85 | Verbose 86 | ------- 87 | This workd same as the ``re.VERBOSE`` flag, which allows you to write regular expressions that look nicer and are more readable by allowing you to visually separate logical sections of the pattern and add comments. However, this flag is basically rendered useless with Edify, but it is still available for use to keep the API consistent with the ``re`` module. Corresponds to the inline flag ``(?x)``. 88 | 89 | 90 | Example 91 | ^^^^^^^ 92 | 93 | .. code-block:: python 94 | 95 | # returns re.compile('hello', re.VERBOSE) 96 | expr = RegexBuilder().verbose().string('hello').to_regex() 97 | -------------------------------------------------------------------------------- /docs/regex-builder/index.rst: -------------------------------------------------------------------------------- 1 | RegexBuilder API Reference 2 | ========================== 3 | 4 | .. toctree:: 5 | :glob: 6 | :maxdepth: 2 7 | 8 | flags/index 9 | builder/index 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | sphinx-rtd-theme 3 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/cover.png -------------------------------------------------------------------------------- /images/logo_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_1500px.png -------------------------------------------------------------------------------- /images/logo_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_500px.png -------------------------------------------------------------------------------- /images/logo_no_text_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_1500px.png -------------------------------------------------------------------------------- /images/logo_no_text_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_500px.png -------------------------------------------------------------------------------- /images/logo_no_text_black_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_black_1500px.png -------------------------------------------------------------------------------- /images/logo_no_text_black_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_black_500px.png -------------------------------------------------------------------------------- /images/logo_no_text_t_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_t_1500px.png -------------------------------------------------------------------------------- /images/logo_no_text_t_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_t_500px.png -------------------------------------------------------------------------------- /images/logo_no_text_t_black_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_t_black_1500px.png -------------------------------------------------------------------------------- /images/logo_no_text_t_black_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_no_text_t_black_500px.png -------------------------------------------------------------------------------- /images/logo_t_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_t_1500px.png -------------------------------------------------------------------------------- /images/logo_t_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_t_500px.png -------------------------------------------------------------------------------- /images/logo_t_black_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_t_black_1500px.png -------------------------------------------------------------------------------- /images/logo_t_black_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_t_black_500px.png -------------------------------------------------------------------------------- /images/logo_wob_1500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_wob_1500px.png -------------------------------------------------------------------------------- /images/logo_wob_500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luciferreeves/edify/b07c5de4091643aa738feeb459e1b71857275d10/images/logo_wob_500px.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=30.3.0", 4 | "wheel", 5 | ] 6 | 7 | [tool.black] 8 | line-length = 140 9 | target-version = ['py37'] 10 | skip-string-normalization = true 11 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # If a pytest section is found in one of the possible config files 3 | # (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, 4 | # so if you add a pytest config section elsewhere, 5 | # you will need to delete this section from setup.cfg. 6 | norecursedirs = 7 | migrations 8 | 9 | python_files = 10 | test_*.py 11 | *_test.py 12 | tests.py 13 | addopts = 14 | -ra 15 | --strict-markers 16 | --doctest-modules 17 | --doctest-glob=\*.rst 18 | --tb=short 19 | testpaths = 20 | tests 21 | 22 | # Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors 23 | filterwarnings = 24 | error 25 | # You can add exclusions, some examples: 26 | # ignore:'edify' defines default_app_config:PendingDeprecationWarning:: 27 | # ignore:The {{% if::: 28 | # ignore:Coverage disabled via --no-cov switch! 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 140 3 | exclude = .tox,.eggs,ci/templates,build,dist 4 | 5 | [tool:isort] 6 | force_single_line = True 7 | line_length = 120 8 | known_first_party = edify 9 | default_section = THIRDPARTY 10 | forced_separate = test_edify 11 | skip = .tox,.eggs,ci/templates,build,dist 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import io 5 | import re 6 | from glob import glob 7 | from os.path import basename 8 | from os.path import dirname 9 | from os.path import join 10 | from os.path import splitext 11 | 12 | from setuptools import find_namespace_packages 13 | from setuptools import setup 14 | 15 | 16 | def read(*names, **kwargs): 17 | with io.open(join(dirname(__file__), *names), encoding=kwargs.get('encoding', 'utf8')) as fh: 18 | return fh.read() 19 | 20 | 21 | setup( 22 | name='edify', 23 | version='0.2.2', 24 | license='Apache-2.0', 25 | description='Regular Expressions Made Simple', 26 | long_description='{}\n{}'.format( 27 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 28 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')), 29 | ), 30 | author='Bobby', 31 | author_email='bobbyskhs@gmail.com', 32 | url='https://github.com/luciferreeves/edify', 33 | packages=find_namespace_packages('src'), 34 | package_dir={'': 'src'}, 35 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 36 | include_package_data=True, 37 | zip_safe=False, 38 | classifiers=[ 39 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 40 | 'Development Status :: 5 - Production/Stable', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: Apache Software License', 43 | 'Operating System :: Unix', 44 | 'Operating System :: POSIX', 45 | 'Operating System :: Microsoft :: Windows', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3 :: Only', 49 | 'Programming Language :: Python :: 3.7', 50 | 'Programming Language :: Python :: 3.8', 51 | 'Programming Language :: Python :: 3.9', 52 | 'Programming Language :: Python :: 3.10', 53 | 'Programming Language :: Python :: 3.11', 54 | 'Programming Language :: Python :: Implementation :: CPython', 55 | 'Programming Language :: Python :: Implementation :: PyPy', 56 | # uncomment if you test on these interpreters: 57 | # 'Programming Language :: Python :: Implementation :: IronPython', 58 | # 'Programming Language :: Python :: Implementation :: Jython', 59 | # 'Programming Language :: Python :: Implementation :: Stackless', 60 | 'Topic :: Utilities', 61 | ], 62 | project_urls={ 63 | 'Documentation': 'https://edify.readthedocs.io/', 64 | 'Changelog': 'https://edify.readthedocs.io/en/latest/changelog.html', 65 | 'Issue Tracker': 'https://github.com/luciferreeves/edify/issues', 66 | }, 67 | keywords=[ 68 | # eg: 'keyword1', 'keyword2', 'keyword3', 69 | ], 70 | python_requires='>=3.7', 71 | install_requires=[ 72 | # eg: 'aspectlib==1.1.1', 'six>=1.7', 73 | ], 74 | extras_require={ 75 | # eg: 76 | # 'rst': ['docutils>=0.11'], 77 | # ':python_version=="2.6"': ['argparse'], 78 | }, 79 | ) 80 | -------------------------------------------------------------------------------- /src/edify/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | __version__ = '0.2.2' 4 | from .builder.builder import RegexBuilder 5 | -------------------------------------------------------------------------------- /src/edify/builder/builder.py: -------------------------------------------------------------------------------- 1 | import re 2 | from copy import deepcopy as clone 3 | 4 | from .errors import can_not_call_se 5 | from .errors import can_not_end_while_building_root_exp 6 | from .errors import cannot_create_duplicate_named_group 7 | from .errors import cannot_define_start_after_end 8 | from .errors import end_input_already_defined 9 | from .errors import ignore_se 10 | from .errors import invalid_total_capture_groups_index 11 | from .errors import must_be_a_string 12 | from .errors import must_be_instance 13 | from .errors import must_be_integer_greater_than_zero 14 | from .errors import must_be_one_character 15 | from .errors import must_be_positive_integer 16 | from .errors import must_be_single_character 17 | from .errors import must_have_a_smaller_value 18 | from .errors import name_not_valid 19 | from .errors import named_group_does_not_exist 20 | from .errors import start_input_already_defined 21 | # from .helpers.core import deep_copy 22 | # from .errors import unable_to_quantify 23 | from .helpers.core import apply_subexpression_defaults 24 | from .helpers.core import assertion 25 | from .helpers.core import create_stack_frame 26 | from .helpers.core import escape_special 27 | from .helpers.core import fuse_elements 28 | from .helpers.quantifiers import quantifier_table 29 | from .helpers.regex_vars import named_group_regex 30 | from .helpers.t import t 31 | 32 | 33 | class RegexBuilder: 34 | """Regular Expression Builder Class.""" 35 | 36 | state = {} 37 | 38 | def __init__(self): 39 | self.state = { 40 | 'has_defined_start': False, 41 | 'has_defined_end': False, 42 | 'flags': { 43 | 'A': False, 44 | 'D': False, 45 | 'I': False, 46 | 'M': False, 47 | 'S': False, 48 | 'X': False, 49 | }, 50 | 'stack': [create_stack_frame(t['root'])], 51 | 'named_groups': [], 52 | 'total_capture_groups': 0, 53 | } 54 | 55 | def ascii_only(self): 56 | next = clone(self) 57 | next.state['flags']['A'] = True 58 | return next 59 | 60 | def debug(self): 61 | next = clone(self) 62 | next.state['flags']['D'] = True 63 | return next 64 | 65 | def ignore_case(self): 66 | next = clone(self) 67 | next.state['flags']['I'] = True 68 | return next 69 | 70 | def multi_line(self): 71 | next = clone(self) 72 | next.state['flags']['M'] = True 73 | return next 74 | 75 | def dot_all(self): 76 | next = clone(self) 77 | next.state['flags']['S'] = True 78 | return next 79 | 80 | def verbose(self): 81 | next = clone(self) 82 | next.state['flags']['X'] = True 83 | return next 84 | 85 | def get_current_frame(self): 86 | return self.state['stack'][len(self.state['stack']) - 1] 87 | 88 | def get_current_element_array(self): 89 | return self.get_current_frame()['elements'] 90 | 91 | def match_element(self, type_fn): 92 | next = clone(self) 93 | next.get_current_element_array().append(next.apply_quantifier(type_fn)) 94 | return next 95 | 96 | def apply_quantifier(self, element): 97 | current_frame = self.get_current_frame() 98 | if current_frame['quantifier'] is not None: 99 | wrapped = current_frame['quantifier']['value'](element) 100 | current_frame['quantifier'] = None 101 | return wrapped 102 | return element 103 | 104 | def frame_creating_element(self, type_fn): 105 | next = clone(self) 106 | new_frame = create_stack_frame(type_fn) 107 | next.state['stack'].append(new_frame) 108 | return next 109 | 110 | def tracked_named_group(self, name): 111 | assertion(type(name) is str, must_be_a_string("Name", type(name))) 112 | assertion(len(name) > 0, must_be_one_character("Name")) 113 | assertion(name not in self.state['named_groups'], cannot_create_duplicate_named_group(name)) 114 | assertion(re.compile(named_group_regex, re.I).match(name), name_not_valid(name)) 115 | self.state['named_groups'].append(name) 116 | 117 | def capture(self): 118 | next = clone(self) 119 | new_frame = create_stack_frame(t['capture']) 120 | next.state['stack'].append(new_frame) 121 | next.state['total_capture_groups'] += 1 122 | return next 123 | 124 | def named_capture(self, name): 125 | next = clone(self) 126 | new_frame = create_stack_frame(t['named_capture'](name)) 127 | next.tracked_named_group(name) 128 | next.state['stack'].append(new_frame) 129 | next.state['total_capture_groups'] += 1 130 | return next 131 | 132 | def quantifier_element(self, type_fn): 133 | next = clone(self) 134 | current_frame = next.get_current_frame() 135 | # if current_frame['quantifier'] is not None: 136 | # raise Exception(unable_to_quantify(type_fn, current_frame['quantifier']['type'])) 137 | current_frame['quantifier'] = t[type_fn] 138 | return next 139 | 140 | def any_char(self): 141 | return self.match_element(t['any_char']) 142 | 143 | def whitespace_char(self): 144 | return self.match_element(t['whitespace_char']) 145 | 146 | def non_whitespace_char(self): 147 | return self.match_element(t['non_whitespace_char']) 148 | 149 | def digit(self): 150 | return self.match_element(t['digit']) 151 | 152 | def non_digit(self): 153 | return self.match_element(t['non_digit']) 154 | 155 | def word(self): 156 | return self.match_element(t['word']) 157 | 158 | def non_word(self): 159 | return self.match_element(t['non_word']) 160 | 161 | def word_boundary(self): 162 | return self.match_element(t['word_boundary']) 163 | 164 | def non_word_boundary(self): 165 | return self.match_element(t['non_word_boundary']) 166 | 167 | def new_line(self): 168 | return self.match_element(t['new_line']) 169 | 170 | def carriage_return(self): 171 | return self.match_element(t['carriage_return']) 172 | 173 | def tab(self): 174 | return self.match_element(t['tab']) 175 | 176 | def null_byte(self): 177 | return self.match_element(t['null_byte']) 178 | 179 | def named_back_reference(self, name): 180 | assertion(name in self.state['named_groups'], named_group_does_not_exist(name)) 181 | return self.match_element(t['named_back_reference'](name)) 182 | 183 | def back_reference(self, index: int): 184 | assertion(type(index) is int, 'Index must be an integer.') 185 | assertion( 186 | index > 0 and index <= self.state['total_capture_groups'], 187 | invalid_total_capture_groups_index(index, self.state['total_capture_groups']), 188 | ) 189 | return self.match_element(t['back_reference'](index)) 190 | 191 | def any_of(self): 192 | return self.frame_creating_element(t['any_of']) 193 | 194 | def group(self): 195 | return self.frame_creating_element(t['group']) 196 | 197 | def assert_ahead(self): 198 | return self.frame_creating_element(t['assert_ahead']) 199 | 200 | def assert_not_ahead(self): 201 | return self.frame_creating_element(t['assert_not_ahead']) 202 | 203 | def assert_behind(self): 204 | return self.frame_creating_element(t['assert_behind']) 205 | 206 | def assert_not_behind(self): 207 | return self.frame_creating_element(t['assert_not_behind']) 208 | 209 | def optional(self): 210 | return self.quantifier_element('optional') 211 | 212 | def zero_or_more(self): 213 | return self.quantifier_element('zero_or_more') 214 | 215 | def zero_or_more_lazy(self): 216 | return self.quantifier_element('zero_or_more_lazy') 217 | 218 | def one_or_more(self): 219 | return self.quantifier_element('one_or_more') 220 | 221 | def one_or_more_lazy(self): 222 | return self.quantifier_element('one_or_more_lazy') 223 | 224 | def exactly(self, count): 225 | assertion(type(count) is int and count > 0, must_be_positive_integer('count')) 226 | current_frame = self.get_current_frame() 227 | # if current_frame['quantifier'] is not None: 228 | # raise Exception(unable_to_quantify("exactly", current_frame['quantifier']['type'])) 229 | current_frame['quantifier'] = t['exactly'](count) 230 | return self 231 | 232 | def at_least(self, count): 233 | assertion(type(count) is int and count > 0, must_be_positive_integer('count')) 234 | next = clone(self) 235 | current_frame = next.get_current_frame() 236 | # if current_frame['quantifier'] is not None: 237 | # raise Exception(unable_to_quantify("at_least", current_frame['quantifier']['type'])) 238 | current_frame['quantifier'] = t['at_least'](count) 239 | return next 240 | 241 | def between(self, x, y): 242 | assertion(type(x) is int and x >= 0, must_be_integer_greater_than_zero('x')) 243 | assertion(type(y) is int and y > 0, must_be_positive_integer('y')) 244 | assertion(x < y, 'X must be less than Y.') 245 | next = clone(self) 246 | current_frame = next.get_current_frame() 247 | # if current_frame['quantifier'] is not None: 248 | # raise Exception(unable_to_quantify("between", current_frame['quantifier']['type'])) 249 | current_frame['quantifier'] = t['between'](x, y) 250 | return next 251 | 252 | def between_lazy(self, x, y): 253 | assertion(type(x) is int and x >= 0, must_be_integer_greater_than_zero('x')) 254 | assertion(type(y) is int and y > 0, must_be_positive_integer('y')) 255 | assertion(x < y, 'X must be less than Y.') 256 | next = clone(self) 257 | current_frame = next.get_current_frame() 258 | # if current_frame['quantifier'] is not None: 259 | # raise Exception(unable_to_quantify("between_lazy", current_frame['quantifier']['type'])) 260 | current_frame['quantifier'] = t['between_lazy'](x, y) 261 | return next 262 | 263 | def start_of_input(self): 264 | assertion(self.state['has_defined_start'] is False, start_input_already_defined()) 265 | assertion(self.state['has_defined_end'] is False, cannot_define_start_after_end()) 266 | next = clone(self) 267 | next.state['has_defined_start'] = True 268 | next.get_current_element_array().append(t['start_of_input']) 269 | return next 270 | 271 | def end_of_input(self): 272 | assertion(self.state['has_defined_end'] is False, end_input_already_defined()) 273 | next = clone(self) 274 | next.state['has_defined_end'] = True 275 | next.get_current_element_array().append(t['end_of_input']) 276 | return next 277 | 278 | def any_of_chars(self, chars): 279 | next = clone(self) 280 | element_value = t['any_of_chars'](escape_special(chars)) 281 | current_frame = next.get_current_frame() 282 | current_frame['elements'].append(next.apply_quantifier(element_value)) 283 | return next 284 | 285 | def end(self): 286 | assertion(len(self.state['stack']) > 1, can_not_end_while_building_root_exp()) 287 | next = clone(self) 288 | old_frame = next.state['stack'].pop() 289 | current_frame = next.get_current_frame() 290 | current_frame['elements'].append(next.apply_quantifier(old_frame['type']['value'](old_frame['elements']))) 291 | return next 292 | 293 | def anything_but_string(self, string): 294 | assertion(type(string) is str, must_be_a_string('Value', string)) 295 | assertion(len(string) > 0, must_be_one_character('Value')) 296 | next = clone(self) 297 | element_value = t['anything_but_string'](escape_special(string)) 298 | current_frame = next.get_current_frame() 299 | current_frame['elements'].append(next.apply_quantifier(element_value)) 300 | return next 301 | 302 | def anything_but_chars(self, chars): 303 | assertion(type(chars) is str, must_be_a_string('Value', chars)) 304 | assertion(len(chars) > 0, must_be_one_character('Value')) 305 | next = clone(self) 306 | element_value = t['anything_but_chars'](escape_special(chars)) 307 | current_frame = next.get_current_frame() 308 | current_frame['elements'].append(next.apply_quantifier(element_value)) 309 | return next 310 | 311 | def anything_but_range(self, a, b): 312 | str_a = str(a) 313 | str_b = str(b) 314 | assertion(len(str_a) == 1, must_be_single_character('a', str_a)) 315 | assertion(len(str_b) == 1, must_be_single_character('b', str_b)) 316 | assertion(ord(str_a) < ord(str_b), must_have_a_smaller_value(str_a, str_b)) 317 | next = clone(self) 318 | element_value = t['anything_but_range']([a, b]) 319 | current_frame = next.get_current_frame() 320 | current_frame['elements'].append(next.apply_quantifier(element_value)) 321 | return next 322 | 323 | def string(self, s): 324 | assertion(type(s) is str, must_be_a_string('Value', s)) 325 | assertion(len(s) > 0, must_be_one_character('Value')) 326 | next = clone(self) 327 | element_value = t['string'](escape_special(s)) if len(s) > 1 else t['char'](escape_special(s)) 328 | current_frame = next.get_current_frame() 329 | current_frame['elements'].append(next.apply_quantifier(element_value)) 330 | return next 331 | 332 | def char(self, c): 333 | assertion(type(c) is str, must_be_a_string('Value', c)) 334 | assertion(len(c) == 1, must_be_single_character('Value', c)) 335 | next = clone(self) 336 | element_value = t['char'](escape_special(c)) 337 | current_frame = next.get_current_frame() 338 | current_frame['elements'].append(next.apply_quantifier(element_value)) 339 | return next 340 | 341 | def range(self, a, b): 342 | str_a = str(a) 343 | str_b = str(b) 344 | assertion(len(str_a) == 1, must_be_single_character('a', str_a)) 345 | assertion(len(str_b) == 1, must_be_single_character('b', str_b)) 346 | assertion(ord(str_a) < ord(str_b), must_have_a_smaller_value(str_a, str_b)) 347 | next = clone(self) 348 | element_value = t['range']([a, b]) 349 | current_frame = next.get_current_frame() 350 | current_frame['elements'].append(next.apply_quantifier(element_value)) 351 | return next 352 | 353 | def merge_subexpression(self, el, options, parent, increment_capture_groups): 354 | next_el = clone(el) 355 | 356 | if next_el['type'] == 'back_reference': 357 | next_el['index'] += parent.state['total_capture_groups'] 358 | if next_el['type'] == 'capture': 359 | increment_capture_groups() 360 | if next_el['type'] == 'named_capture': 361 | group_name = '{}{}'.format(options['namespace'], next_el['name']) if options['namespace'] else next_el['name'] 362 | # parent.tracked_named_group(group_name) 363 | next_el['name'] = group_name 364 | if next_el['type'] == 'named_back_reference': 365 | next_el['name'] = '{}{}'.format(options['namespace'], next_el['name']) if options['namespace'] else next_el['name'] 366 | if 'contains_child' in next_el: 367 | next_el['value'] = self.merge_subexpression(next_el['value'], options, parent, increment_capture_groups) 368 | elif 'contains_children' in next_el: 369 | next_el['value'] = list(map(lambda e: self.merge_subexpression(e, options, parent, increment_capture_groups), next_el['value'])) 370 | if next_el['type'] == 'start_of_input': 371 | if options['ignore_start_and_end']: 372 | return t['noop'] 373 | assertion(parent.state['has_defined_start'] is False, str(start_input_already_defined()) + " " + str(ignore_se())) 374 | # assertion(parent.state['has_defined_end'] is False, str(end_input_already_defined()) + " " + str(ignore_se())) 375 | # parent.state['has_defined_start'] = True 376 | if next_el['type'] == 'end_of_input': 377 | if options['ignore_start_and_end']: 378 | return t['noop'] 379 | assertion(parent.state['has_defined_end'] is False, str(end_input_already_defined()) + str(ignore_se())) 380 | # parent.state['has_defined_end'] = True 381 | return next_el 382 | 383 | def subexpression(self, expr, opts={}): 384 | assertion(isinstance(expr, RegexBuilder), must_be_instance("Expression", expr, "RegexBuilder")) 385 | assertion(len(expr.state['stack']) == 1, can_not_call_se(expr.get_current_frame()['type']['type'])) 386 | options = apply_subexpression_defaults(opts) 387 | expr_next = clone(expr) 388 | next = clone(self) 389 | additional_capture_groups = 0 390 | expr_frame = expr_next.get_current_frame() 391 | 392 | def increment_capture_groups(): 393 | nonlocal additional_capture_groups 394 | additional_capture_groups += 1 395 | 396 | expr_frame['elements'] = list( 397 | map(lambda e: self.merge_subexpression(e, options, expr_next, increment_capture_groups), expr_frame['elements']) 398 | ) 399 | next.state['total_capture_groups'] += additional_capture_groups 400 | if not options['ignore_flags']: 401 | for flag_name, enabled in expr_next.state['flags'].items(): 402 | next.state['flags'][flag_name] = enabled or next.state['flags'][flag_name] 403 | current_frame = next.get_current_frame() 404 | current_frame['elements'].append(next.apply_quantifier(t['subexpression'](expr_frame['elements']))) 405 | return next 406 | 407 | def evaluate(self, el): 408 | if el['type'] == 'noop': 409 | return '' 410 | if el['type'] == 'any_char': 411 | return '.' 412 | if el['type'] == 'whitespace_char': 413 | return '\\s' 414 | if el['type'] == 'non_whitespace_char': 415 | return '\\S' 416 | if el['type'] == 'digit': 417 | return '\\d' 418 | if el['type'] == 'non_digit': 419 | return '\\D' 420 | if el['type'] == 'word': 421 | return '\\w' 422 | if el['type'] == 'non_word': 423 | return '\\W' 424 | if el['type'] == 'word_boundary': 425 | return '\\b' 426 | if el['type'] == 'non_word_boundary': 427 | return '\\B' 428 | if el['type'] == 'start_of_input': 429 | return '^' 430 | if el['type'] == 'end_of_input': 431 | return '$' 432 | if el['type'] == 'new_line': 433 | return '\\n' 434 | if el['type'] == 'carriage_return': 435 | return '\\r' 436 | if el['type'] == 'tab': 437 | return '\\t' 438 | if el['type'] == 'null_byte': 439 | return '\\0' 440 | if el['type'] == 'string': 441 | return el['value'] 442 | if el['type'] == 'char': 443 | return el['value'] 444 | if el['type'] == 'range': 445 | return '[{}-{}]'.format(el['value'][0], el['value'][1]) 446 | if el['type'] == 'anything_but_range': 447 | return '[^{}-{}]'.format(el['value'][0], el['value'][1]) 448 | if el['type'] == 'any_of_chars': 449 | return '[' + ''.join(el['value']) + ']' 450 | if el['type'] == 'anything_but_chars': 451 | return '[^' + ''.join(el['value']) + ']' 452 | if el['type'] == 'named_back_reference': 453 | return '\\k<{}>'.format(el['name']) 454 | if el['type'] == 'back_reference': 455 | return '\\{}'.format(el['index']) 456 | if el['type'] == 'subexpression': 457 | return ''.join(map(lambda e: self.evaluate(e), el['value'])) 458 | cg1 = ['optional', 'zero_or_more', 'zero_or_more_lazy', 'one_or_more', 'one_or_more_lazy'] 459 | if el['type'] in cg1: 460 | inner = self.evaluate(el['value']) 461 | with_group = "(?:{})".format(inner) if 'quantifiers_require_group' in el['value'] else inner 462 | symbol = quantifier_table[el['type']] 463 | return '{}{}'.format(with_group, symbol) 464 | cg2 = ['between', 'between_lazy', 'at_least', 'exactly'] 465 | if el['type'] in cg2: 466 | inner = self.evaluate(el['value']) 467 | with_group = "(?:{})".format(inner) if 'quantifiers_require_group' in el['value'] else inner 468 | return '{}{}'.format(with_group, quantifier_table[el['type']](el['times'])) 469 | if el['type'] == 'anything_but_string': 470 | chars = ''.join(map(lambda c: '[^{}]'.format(c), el['value'])) 471 | return '(?:{})'.format(chars) 472 | if el['type'] == 'assert_ahead': 473 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 474 | return '(?={})'.format(evaluated) 475 | if el['type'] == 'assert_behind': 476 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 477 | return '(?<={})'.format(evaluated) 478 | if el['type'] == 'assert_not_ahead': 479 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 480 | return '(?!{})'.format(evaluated) 481 | if el['type'] == 'assert_not_behind': 482 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 483 | return '(? 0 and len(fused) > 0 else '' 490 | return '(?:{}{}{})'.format('|'.join(evaluated_rest), separator, '[{}]'.format(fused) if fused else '') 491 | if el['type'] == 'capture': 492 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 493 | return '({})'.format(evaluated) 494 | if el['type'] == 'named_capture': 495 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 496 | return '(?P<{}>{})'.format(el['name'], evaluated) 497 | if el['type'] == 'group': 498 | evaluated = ''.join(map(lambda e: self.evaluate(e), el['value'])) 499 | return '(?:{})'.format(evaluated) 500 | 501 | raise Exception('Can not process unsupported element type: {}'.format(el['type'])) # pragma: no cover 502 | 503 | def get_regex_patterns_and_flags(self): 504 | assertion(len(self.state['stack']) == 1, can_not_call_se(self.get_current_frame()['type']['type'])) 505 | pattern = "".join(list(map(lambda e: self.evaluate(e), self.get_current_element_array()))) 506 | flags = "" 507 | for flag_name, enabled in self.state['flags'].items(): 508 | if enabled: 509 | flags += flag_name 510 | pattern = "(?:)" if pattern == "" else pattern 511 | flags = "".join(sorted(flags)) 512 | return pattern, flags 513 | 514 | def to_regex_string(self): 515 | patterns, flags = self.get_regex_patterns_and_flags() 516 | return '/{}/{}'.format(str(patterns.replace('\\ ', ' ')), str(flags)) 517 | 518 | def to_regex(self): 519 | patterns, flags = self.get_regex_patterns_and_flags() 520 | patterns = r"{}".format(patterns.replace("\\ ", ' ')) 521 | flag = 0 522 | if flags != '': 523 | for flag_name in flags: 524 | if flag_name == 'D': 525 | flag |= getattr(re, 'DEBUG') 526 | else: 527 | flag |= getattr(re, flag_name) 528 | 529 | try: 530 | return re.compile(patterns, flags=flag) 531 | except Exception as e: 532 | raise Exception('Can not compile regex: {}'.format(e)) 533 | -------------------------------------------------------------------------------- /src/edify/builder/errors.py: -------------------------------------------------------------------------------- 1 | def must_be_a_string(value, variable_name): 2 | return '{} must be a string. (got {})'.format(value, type(variable_name)) 3 | 4 | 5 | def must_be_one_character(variable_name): 6 | return '{} must be one character long.'.format(variable_name) 7 | 8 | 9 | def cannot_create_duplicate_named_group(name): 10 | return 'Can not create duplicate named group "{}".'.format(name) 11 | 12 | 13 | def name_not_valid(name): 14 | return 'Name {} is not valid. (only alphanumeric characters and underscores are allowed)'.format(name) 15 | 16 | 17 | def named_group_does_not_exist(name): 18 | return 'Named group "{}" does not exist (create one with .named_capture()).'.format(name) 19 | 20 | 21 | def invalid_total_capture_groups_index(index, total_capture_groups): 22 | return 'Invalid index #{}. There are only {} capture groups.'.format(index, total_capture_groups) 23 | 24 | 25 | def must_be_positive_integer(variable_name): 26 | return '{} must be a positive integer.'.format(variable_name) 27 | 28 | 29 | def must_be_integer_greater_than_zero(variable_name): 30 | return '{} must be an integer greater than zero.'.format(variable_name) 31 | 32 | 33 | # def unable_to_quantify(quantifier, type): 34 | # return 'Can not quantify regular expression with {}, because it has already been quantified with {}.'.format(quantifier, type) 35 | 36 | 37 | def start_input_already_defined(): 38 | return 'Regex already has a start of input.' 39 | 40 | 41 | def cannot_define_start_after_end(): 42 | return 'Can not define a start of input after defining an end of input.' 43 | 44 | 45 | def end_input_already_defined(): 46 | return 'Regex already has an end of input.' 47 | 48 | 49 | def can_not_end_while_building_root_exp(): 50 | return 'Can not end while building the root expression.' 51 | 52 | 53 | def must_be_single_character(value, variable_name): 54 | return '{} must be a single character. (got {})'.format(value, type(variable_name)) 55 | 56 | 57 | def must_have_a_smaller_value(a, b): 58 | return '{} must have a smaller character value than {}. (a = {}, b = {})'.format(a, b, ord(a), ord(b)) 59 | 60 | 61 | def ignore_se(): 62 | return 'You can ignore a subexpressions start_of_input/end_of_input markers with the ignore_start_and_end option' 63 | 64 | 65 | def must_be_instance(value, variable_name, class_name): 66 | return '{} must be an instance of {}. (got {})'.format(value, class_name, type(variable_name)) 67 | 68 | 69 | def can_not_call_se(cft): 70 | return "Can not call subexpression a not yet fully specified regex object. \ 71 | \n (Try adding a .end() call to match the {} on the subexpression)".format( 72 | cft 73 | ) 74 | -------------------------------------------------------------------------------- /src/edify/builder/helpers/core.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def as_type(type, opts={}): 5 | def type_fn(value=None): 6 | return { 7 | 'type': type, 8 | 'value': value, 9 | **opts, 10 | } 11 | 12 | return type_fn 13 | 14 | 15 | def deferred_type(type, opts={}): 16 | type_fn = as_type(type, opts) 17 | return type_fn(type_fn) 18 | 19 | 20 | def create_stack_frame(type): 21 | return { 22 | 'type': type, 23 | 'quantifier': None, 24 | 'elements': [], 25 | } 26 | 27 | 28 | def assertion(condition, message): 29 | if not condition: 30 | raise Exception(message) 31 | 32 | 33 | def escape_special(s): 34 | return re.escape(s) 35 | 36 | 37 | # def deep_copy(o): 38 | # if isinstance(o, list): 39 | # return [deep_copy(e) for e in o] 40 | # if isinstance(o, dict): 41 | # return {k: deep_copy(v) for k, v in o.items()} 42 | # return o 43 | 44 | 45 | def apply_subexpression_defaults(expr): 46 | out = {**expr} 47 | out['namespace'] = "" if 'namespace' not in out else out['namespace'] 48 | out['ignore_flags'] = True if 'ignore_flags' not in out else out['ignore_flags'] 49 | out['ignore_start_and_end'] = True if 'ignore_start_and_end' not in out else out['ignore_start_and_end'] 50 | assertion(type(out['namespace']) == str, 'namespace must be a string') 51 | assertion(type(out['ignore_flags']) == bool, 'ignore_flags must be a boolean') 52 | assertion(type(out['ignore_start_and_end']) == bool, 'ignore_start_and_end must be a boolean') 53 | return out 54 | 55 | 56 | def is_fusable(element): 57 | return element['type'] == 'range' or element['type'] == 'char' or element['type'] == 'any_of_chars' 58 | 59 | 60 | def partition(pred, a): 61 | result = [[], []] 62 | for cur in a: 63 | if pred(cur): 64 | result[0].append(cur) 65 | else: 66 | result[1].append(cur) 67 | return result 68 | 69 | 70 | def fuse_elements(elements): 71 | [fusables, rest] = partition(is_fusable, elements) 72 | 73 | def map_el(el): 74 | if el['type'] == 'char' or el['type'] == 'any_of_chars': 75 | return el['value'] 76 | return '{}-{}'.format(el['value'][0], el['value'][1]) 77 | 78 | fused = ''.join(map(map_el, fusables)) 79 | return [fused, rest] 80 | -------------------------------------------------------------------------------- /src/edify/builder/helpers/quantifiers.py: -------------------------------------------------------------------------------- 1 | quantifier_table = { 2 | 'one_or_more': '+', 3 | 'one_or_more_lazy': '+?', 4 | 'zero_or_more': '*', 5 | 'zero_or_more_lazy': '*?', 6 | 'optional': '?', 7 | 'exactly': lambda times: '{{{}}}'.format(times), 8 | 'at_least': lambda times: '{{{},}}'.format(times), 9 | 'between': lambda times: '{{{},{}}}'.format(times[0], times[1]), 10 | 'between_lazy': lambda times: '{{{},{}}}?'.format(times[0], times[1]), 11 | } 12 | -------------------------------------------------------------------------------- /src/edify/builder/helpers/regex_vars.py: -------------------------------------------------------------------------------- 1 | named_group_regex = r"^[a-z]+\w*$" 2 | -------------------------------------------------------------------------------- /src/edify/builder/helpers/t.py: -------------------------------------------------------------------------------- 1 | from .core import as_type 2 | from .core import deferred_type 3 | 4 | t = { 5 | 'root': as_type('root')(), 6 | 'noop': as_type('noop')(), 7 | 'start_of_input': as_type('start_of_input')(), 8 | 'end_of_input': as_type('end_of_input')(), 9 | 'any_char': as_type('any_char')(), 10 | 'whitespace_char': as_type('whitespace_char')(), 11 | 'non_whitespace_char': as_type('non_whitespace_char')(), 12 | 'digit': as_type('digit')(), 13 | 'non_digit': as_type('non_digit')(), 14 | 'word': as_type('word')(), 15 | 'non_word': as_type('non_word')(), 16 | 'word_boundary': as_type('word_boundary')(), 17 | 'non_word_boundary': as_type('non_word_boundary')(), 18 | 'new_line': as_type('new_line')(), 19 | 'carriage_return': as_type('carriage_return')(), 20 | 'tab': as_type('tab')(), 21 | 'null_byte': as_type('null_byte')(), 22 | 'any_of_chars': as_type('any_of_chars'), 23 | 'anything_but_string': as_type('anything_but_string'), 24 | 'anything_but_chars': as_type('anything_but_chars'), 25 | 'anything_but_range': as_type('anything_but_range'), 26 | 'char': as_type('char'), 27 | 'range': as_type('range'), 28 | 'string': as_type('string', {'quantifiers_require_group': True}), 29 | 'named_back_reference': lambda name: deferred_type('named_back_reference', {'name': name}), 30 | 'back_reference': lambda index: deferred_type('back_reference', {'index': index}), 31 | 'capture': deferred_type('capture', {'contains_children': True}), 32 | 'subexpression': as_type('subexpression', {'contains_children': True, 'quantifiers_require_group': True}), 33 | 'named_capture': lambda name: deferred_type('named_capture', {'name': name, 'contains_children': True}), 34 | 'group': deferred_type('group', {'contains_children': True}), 35 | 'any_of': deferred_type('any_of', {'contains_children': True}), 36 | 'assert_ahead': deferred_type('assert_ahead', {'contains_children': True}), 37 | 'assert_not_ahead': deferred_type('assert_not_ahead', {'contains_children': True}), 38 | 'assert_behind': deferred_type('assert_behind', {'contains_children': True}), 39 | 'assert_not_behind': deferred_type('assert_not_behind', {'contains_children': True}), 40 | 'exactly': lambda times: deferred_type('exactly', {'times': times, 'contains_child': True}), 41 | 'at_least': lambda times: deferred_type('at_least', {'times': times, 'contains_child': True}), 42 | 'between': lambda x, y: deferred_type('between', {'times': [x, y], 'contains_child': True}), 43 | 'between_lazy': lambda x, y: deferred_type('between_lazy', {'times': [x, y], 'contains_child': True}), 44 | 'zero_or_more': deferred_type('zero_or_more', {'contains_child': True}), 45 | 'zero_or_more_lazy': deferred_type('zero_or_more_lazy', {'contains_child': True}), 46 | 'one_or_more': deferred_type('one_or_more', {'contains_child': True}), 47 | 'one_or_more_lazy': deferred_type('one_or_more_lazy', {'contains_child': True}), 48 | 'optional': deferred_type('optional', {'contains_child': True}), 49 | } 50 | -------------------------------------------------------------------------------- /src/edify/library/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | # Import everything from the library. 4 | from .date import date 5 | from .date import iso_date 6 | from .guid import guid 7 | from .ip import ipv4 8 | from .ip import ipv6 9 | from .mac import mac 10 | from .mail import email 11 | from .mail import email_rfc_5322 12 | from .password import password 13 | from .phone import phone_number 14 | from .ssn import ssn 15 | from .url import url 16 | from .uuid import uuid 17 | from .zip import zip 18 | -------------------------------------------------------------------------------- /src/edify/library/date.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | date_pattern = "^[0-9]{1,2}\\/[0-9]{1,2}\\/[0-9]{4}$" 4 | iso_date_pattern = "^(?:\\d{4})-(?:\\d{2})-(?:\\d{2})T(?:\\d{2}):(?:\\d{2}):(?:\\d{2}(?:\\.\\d*)?)(?:(?:-(?:\\d{2}):(?:\\d{2})|Z)?)$" # noqa 5 | 6 | 7 | def date(date: str) -> bool: 8 | """Checks if a string is a valid date. 9 | 10 | Args: 11 | date (str): The string to check. 12 | Returns: 13 | bool: True if the string is a valid date, False otherwise. 14 | """ 15 | 16 | if re.match(date_pattern, date): 17 | return True 18 | else: 19 | return False 20 | 21 | 22 | def iso_date(date: str) -> bool: 23 | """Checks if a string is a valid ISO date. 24 | 25 | Args: 26 | date (str): The string to check. 27 | Returns: 28 | bool: True if the string is a valid ISO date, False otherwise. 29 | """ 30 | 31 | if re.match(iso_date_pattern, date): 32 | return True 33 | else: 34 | return False 35 | -------------------------------------------------------------------------------- /src/edify/library/guid.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | pattern = "^(?:\\{{0,1}(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}\\}{0,1})$" 4 | 5 | 6 | def guid(guid: str) -> bool: 7 | """Check if the given string is a valid GUID. 8 | 9 | Args: 10 | guid (str): The string to check. 11 | 12 | Returns: 13 | bool: True if the string is a valid GUID, False otherwise. 14 | """ 15 | return bool(re.match(pattern, guid)) 16 | -------------------------------------------------------------------------------- /src/edify/library/ip.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | ipv4_pattern = "^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" # noqa 4 | ipv6_pattern = "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" # noqa 5 | 6 | 7 | def ipv4(ip: str) -> bool: 8 | """Checks if a string is a valid IPv4 address. 9 | 10 | Args: 11 | ip (str): The string to check. 12 | Returns: 13 | bool: True if the string is a valid IPv4 address, False otherwise. 14 | """ 15 | 16 | if re.match(ipv4_pattern, ip): 17 | return True 18 | else: 19 | return False 20 | 21 | 22 | def ipv6(ip: str) -> bool: 23 | """Checks if a string is a valid IPv6 address. 24 | 25 | Args: 26 | ip (str): The string to check. 27 | Returns: 28 | bool: True if the string is a valid IPv6 address, False otherwise. 29 | """ 30 | 31 | if re.match(ipv6_pattern, ip): 32 | return True 33 | else: 34 | return False 35 | -------------------------------------------------------------------------------- /src/edify/library/mac.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | mac_address_validate_pattern = "^(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})$" 4 | 5 | 6 | def mac(mac: str) -> bool: 7 | """Validate a MAC (IEEE 802) address. 8 | 9 | Args: 10 | mac (str): The MAC address to validate. 11 | 12 | Returns: 13 | bool: True if the MAC address is valid, False otherwise. 14 | """ 15 | if not isinstance(mac, str): 16 | return False 17 | return bool(re.match(mac_address_validate_pattern, mac)) 18 | -------------------------------------------------------------------------------- /src/edify/library/mail.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | pattern = r"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$" # noqa 4 | rfc_5322_pattern = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])" # noqa 5 | 6 | 7 | def email(email: str) -> bool: 8 | """Checks if a string is a valid email address. 9 | 10 | Args: 11 | email (str): The string to check. 12 | Returns: 13 | bool: True if the string is a valid email address, False otherwise. 14 | """ 15 | 16 | if re.match(pattern, email): 17 | return True 18 | else: 19 | return False 20 | 21 | 22 | def email_rfc_5322(email: str) -> bool: 23 | """Checks if a string is a valid email address. 24 | 25 | Args: 26 | email (str): The string to check. 27 | Returns: 28 | bool: True if the string is a valid email address, False otherwise. 29 | """ 30 | 31 | if re.match(rfc_5322_pattern, email): 32 | return True 33 | else: 34 | return False 35 | -------------------------------------------------------------------------------- /src/edify/library/password.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def password( 5 | password: str, 6 | min_length: int = 8, 7 | max_length: int = 64, 8 | min_upper: int = 1, 9 | min_lower: int = 1, 10 | min_digit: int = 1, 11 | min_special: int = 1, 12 | special_chars: str = "!@#$%^&*()_+-=[]{}|;':\",./<>?", 13 | ) -> bool: 14 | """Check if the given string is a valid password. 15 | 16 | Args: 17 | password (str): The string to check. 18 | min_length (int): The minimum length of the password. 19 | max_length (int): The maximum length of the password. 20 | min_upper (int): The minimum number of upper case characters. 21 | min_lower (int): The minimum number of lower case characters. 22 | min_digit (int): The minimum number of digits. 23 | min_special (int): The minimum number of special characters. 24 | special_chars (str): The special characters to check for. 25 | 26 | Returns: 27 | bool: True if the string is a valid password, False otherwise. 28 | """ 29 | if len(password) < min_length or len(password) > max_length: 30 | return False 31 | 32 | upper = re.findall("[A-Z]", password or "") 33 | lower = re.findall("[a-z]", password or "") 34 | digit = re.findall("[0-9]", password or "") 35 | special = [c for c in password if c in special_chars] 36 | 37 | if len(upper) < min_upper or len(lower) < min_lower or len(digit) < min_digit or len(special) < min_special: 38 | return False 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /src/edify/library/phone.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | pattern = "^\\+?\\d{1,4}?[-.\\s]?\\(?\\d{1,3}?\\)?[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}$" 4 | special_pattern = r"^\d{2,4}" 5 | 6 | 7 | def phone_number(phone: str) -> bool: 8 | """Checks if a string is a valid phone number. 9 | 10 | Args: 11 | phone (str): The string to check. 12 | Returns: 13 | bool: True if the string is a valid phone number, False otherwise. 14 | """ 15 | 16 | if re.match(pattern, phone) or re.match(special_pattern, phone): 17 | return True 18 | else: 19 | return False 20 | -------------------------------------------------------------------------------- /src/edify/library/ssn.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | ssn_validate_pattern = "^(?!666|000|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0{4})\\d{4}$" 4 | 5 | 6 | def ssn(ssn: str) -> bool: 7 | """Validate a Social Security Number (SSN). 8 | 9 | Args: 10 | ssn (str): The SSN to validate. 11 | 12 | Returns: 13 | bool: True if the SSN is valid, False otherwise. 14 | """ 15 | if not isinstance(ssn, str): 16 | return False 17 | return bool(re.match(ssn_validate_pattern, ssn)) 18 | -------------------------------------------------------------------------------- /src/edify/library/support/zip.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | ZIP_LOCALES = [ 4 | {"abbrev": "AF", "name": "Afghanistan", "zip": "[0-9]{4}"}, 5 | {"abbrev": "AL", "name": "Albania", "zip": "(120|122)[0-9]{2}"}, 6 | {"abbrev": "DZ", "name": "Algeria", "zip": "[0-9]{5}"}, 7 | {"abbrev": "AS", "name": "American Samoa", "zip": "[0-9]{5}"}, 8 | {"abbrev": "AD", "name": "Andorra", "zip": "[0-9]{5}"}, 9 | {"abbrev": "AO", "name": "Angola"}, 10 | {"abbrev": "AI", "name": "Anguilla", "zip": "AI-2640"}, 11 | {"abbrev": "AG", "name": "Antigua and Barbuda"}, 12 | {"abbrev": "AR", "name": "Argentina", "zip": "[A-Z]{1}[0-9]{4}[A-Z]{3}"}, 13 | {"abbrev": "AM", "name": "Armenia", "zip": "[0-9]{4}"}, 14 | {"abbrev": "AW", "name": "Aruba"}, 15 | {"abbrev": "AU", "name": "Australia", "zip": "[0-9]{4}"}, 16 | {"abbrev": "AT", "name": "Austria", "zip": "[0-9]{4}"}, 17 | {"abbrev": "AZ", "name": "Azerbaijan", "zip": "[0-9]{4}"}, 18 | {"abbrev": "BS", "name": "Bahamas"}, 19 | {"abbrev": "BH", "name": "Bahrain"}, 20 | {"abbrev": "BD", "name": "Bangladesh", "zip": "[0-9]{4}"}, 21 | {"abbrev": "BB", "name": "Barbados", "zip": "BB[0-9]{5}"}, 22 | {"abbrev": "BY", "name": "Belarus", "zip": "[0-9]{6}"}, 23 | {"abbrev": "BE", "name": "Belgium", "zip": "[0-9]{4}"}, 24 | {"abbrev": "BZ", "name": "Belize"}, 25 | {"abbrev": "BJ", "name": "Benin"}, 26 | {"abbrev": "BM", "name": "Bermuda", "zip": "[A-Z]{2}[0-9]{2}"}, 27 | {"abbrev": "BT", "name": "Bhutan", "zip": "[0-9]{5}"}, 28 | {"abbrev": "BO", "name": "Bolivia"}, 29 | {"abbrev": "BQ", "name": "Bonaire"}, 30 | {"abbrev": "BA", "name": "Bosnia and Herzegovina", "zip": "[0-9]{5}"}, 31 | {"abbrev": "BW", "name": "Botswana"}, 32 | {"abbrev": "BR", "name": "Brazil", "zip": "[0-9]{5}-[0-9]{3}"}, 33 | {"abbrev": "BN", "name": "Brunei", "zip": "[A-Z]{2}[0-9]{4}"}, 34 | {"abbrev": "BG", "name": "Bulgaria", "zip": "[0-9]{4}"}, 35 | {"abbrev": "BF", "name": "Burkina Faso"}, 36 | {"abbrev": "BI", "name": "Burundi"}, 37 | {"abbrev": "KH", "name": "Cambodia", "zip": "[0-9]{5}"}, 38 | {"abbrev": "CM", "name": "Cameroon"}, 39 | {"abbrev": "CA", "name": "Canada", "zip": "[A-Z][0-9][A-Z] ?[0-9][A-Z][0-9]"}, 40 | {"abbrev": "CI", "name": "Canary Islands", "zip": "[0-9]{5}"}, 41 | {"abbrev": "CV", "name": "Cape Verde", "zip": "[0-9]{4}"}, 42 | {"abbrev": "KY", "name": "Cayman Islands", "zip": "[A-Z]{2}[0-9]-[0-9]{4}"}, 43 | {"abbrev": "CF", "name": "Central African Republic"}, 44 | {"abbrev": "TD", "name": "Chad"}, 45 | {"abbrev": "CI", "name": "Channel Islands", "zip": "[A-Z]{2}[0-9]{2}"}, 46 | {"abbrev": "CL", "name": "Chile", "zip": "[0-9]{7}"}, 47 | {"abbrev": "CN", "name": "China, People's Republic", "zip": "[0-9]{6}"}, 48 | {"abbrev": "CO", "name": "Colombia", "zip": "[0-9]{6}"}, 49 | {"abbrev": "KM", "name": "Comoros"}, 50 | {"abbrev": "CG", "name": "Congo"}, 51 | {"abbrev": "CD", "name": "Congo, The Democratic Republic of"}, 52 | {"abbrev": "CK", "name": "Cook Islands"}, 53 | {"abbrev": "CR", "name": "Costa Rica", "zip": "[0-9]{5}"}, 54 | {"abbrev": "CI", "name": "Côte d'Ivoire"}, 55 | {"abbrev": "HR", "name": "Croatia", "zip": "[0-9]{5}"}, 56 | {"abbrev": "CU", "name": "Cuba", "zip": "[0-9]{5}"}, 57 | {"abbrev": "CW", "name": "Curacao"}, 58 | {"abbrev": "CY", "name": "Cyprus", "zip": "[0-9]{4}"}, 59 | {"abbrev": "CZ", "name": "Czech Republic", "zip": "[0-9]{3} [0-9]{2}"}, 60 | {"abbrev": "DK", "name": "Denmark", "zip": "[0-9]{5}"}, 61 | {"abbrev": "DJ", "name": "Djibouti"}, 62 | {"abbrev": "DM", "name": "Dominica"}, 63 | {"abbrev": "DO", "name": "Dominican Republic", "zip": "[0-9]{5}"}, 64 | {"abbrev": "TL", "name": "East Timor"}, 65 | {"abbrev": "EC", "name": "Ecuador", "zip": "[0-9]{6}"}, 66 | {"abbrev": "EG", "name": "Egypt", "zip": "[0-9]{5}"}, 67 | {"abbrev": "SV", "name": "El Salvador", "zip": "[0-9]{4}"}, 68 | {"abbrev": "ER", "name": "Eritrea"}, 69 | {"abbrev": "EE", "name": "Estonia", "zip": "[0-9]{5}"}, 70 | {"abbrev": "ET", "name": "Ethiopia", "zip": "[0-9]{4}"}, 71 | {"abbrev": "FK", "name": "Falkland Islands", "zip": "FIQQ 1ZZ"}, 72 | {"abbrev": "FO", "name": "Faroe Islands", "zip": "[0-9]{3}"}, 73 | {"abbrev": "FJ", "name": "Fiji"}, 74 | {"abbrev": "FI", "name": "Finland", "zip": "[0-9]{5}"}, 75 | {"abbrev": "FR", "name": "France", "zip": "[0-9]{5}"}, 76 | {"abbrev": "PF", "name": "French Polynesia", "zip": "987[0-9]{2}", "range": ["98700", "98790"]}, 77 | {"abbrev": "GA", "name": "Gabon"}, 78 | {"abbrev": "GM", "name": "Gambia"}, 79 | {"abbrev": "GE", "name": "Georgia"}, 80 | {"abbrev": "DE", "name": "Germany", "zip": "[0-9]{5}"}, 81 | {"abbrev": "GH", "name": "Ghana"}, 82 | {"abbrev": "GI", "name": "Gibraltar", "zip": "GX11 1AA"}, 83 | {"abbrev": "GR", "name": "Greece", "zip": "[0-9]{3} [0-9]{2}"}, 84 | {"abbrev": "GL", "name": "Greenland", "zip": "[0-9]{4}"}, 85 | {"abbrev": "GD", "name": "Grenada"}, 86 | {"abbrev": "GP", "name": "Guadeloupe", "zip": "971[0-9]{2}", "range": ["97100", "97190"]}, 87 | {"abbrev": "GU", "name": "Guam", "zip": "\\d{5}(?:[-\\s]\\d{4})?", "range": ["96910", "96932"]}, 88 | {"abbrev": "GT", "name": "Guatemala", "zip": "[0-9]{5}"}, 89 | { 90 | "abbrev": "GG", 91 | "name": "Guernsey", 92 | "zip": "([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?))))\\s?[0-9][A-Za-z]{2})", 93 | }, 94 | {"abbrev": "GW", "name": "Guinea-Bissau", "zip": "[0-9]{4}"}, 95 | {"abbrev": "GQ", "name": "Guinea-Equatorial"}, 96 | {"abbrev": "GN", "name": "Guinea Republic", "zip": "[0-9]{3}"}, 97 | {"abbrev": "GY", "name": "Guyana (British)"}, 98 | {"abbrev": "GF", "name": "Guyana (French)", "zip": "973[0-9]{2}", "range": ["97300", "97390"]}, 99 | {"abbrev": "HT", "name": "Haiti", "zip": "[0-9]{4}"}, 100 | {"abbrev": "HN", "name": "Honduras", "zip": "[0-9]{5}"}, 101 | {"abbrev": "HK", "name": "Hong Kong"}, 102 | {"abbrev": "HU", "name": "Hungary", "zip": "[0-9]{4}"}, 103 | {"abbrev": "IS", "name": "Iceland", "zip": "[0-9]{3}"}, 104 | {"abbrev": "IN", "name": "India", "zip": "^(?!0{1})\d{6}$"}, 105 | {"abbrev": "ID", "name": "Indonesia", "zip": "[0-9]{5}"}, 106 | {"abbrev": "IR", "name": "Iran", "zip": "[0-9]{5}"}, 107 | {"abbrev": "IQ", "name": "Iraq", "zip": "[0-9]{5}"}, 108 | {"abbrev": "IE", "name": "Ireland, Republic of", "zip": "(?:^[AC-FHKNPRTV-Y][0-9]{2}|D6W)[ -]?[0-9AC-FHKNPRTV-Y]{4}$"}, 109 | {"abbrev": "FK", "name": "Islas Malvinas", "zip": "FIQQ 1ZZ"}, 110 | {"abbrev": "IL", "name": "Israel", "zip": "[0-9]{5}|[0-9]{7}"}, 111 | {"abbrev": "IT", "name": "Italy", "zip": "[0-9]{5}"}, 112 | {"abbrev": "CI", "name": "Ivory Coast"}, 113 | {"abbrev": "JM", "name": "Jamaica"}, 114 | {"abbrev": "JP", "name": "Japan", "zip": "[0-9]{3}-[0-9]{4}"}, 115 | { 116 | "abbrev": "JE", 117 | "name": "Jersey", 118 | "zip": "([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?))))\\s?[0-9][A-Za-z]{2})", 119 | }, 120 | {"abbrev": "JO", "name": "Jordan", "zip": "[0-9]{5}"}, 121 | {"abbrev": "KZ", "name": "Kazakhstan", "zip": "[0-9]{6}"}, 122 | {"abbrev": "KE", "name": "Kenya", "zip": "[0-9]{5}"}, 123 | {"abbrev": "KI", "name": "Kiribati"}, 124 | {"abbrev": "KR", "name": "Korea, Republic of", "zip": "[0-9]{5}"}, 125 | {"abbrev": "KP", "name": "Korea, The D.P.R of"}, 126 | {"abbrev": "XK", "name": "Kosovo", "zip": "[0-9]{5}"}, 127 | {"abbrev": "KW", "name": "Kuwait", "zip": "[0-9]{5}"}, 128 | {"abbrev": "KG", "name": "Kyrgyzstan", "zip": "[0-9]{6}"}, 129 | {"abbrev": "LA", "name": "Laos", "zip": "[0-9]{5}"}, 130 | {"abbrev": "LV", "name": "Latvia", "zip": "LV-[0-9]{4}"}, 131 | {"abbrev": "LB", "name": "Lebanon", "zip": "[0-9]{4} [0-9]{4}"}, 132 | {"abbrev": "LS", "name": "Lesotho", "zip": "[0-9]{3}"}, 133 | {"abbrev": "LR", "name": "Liberia", "zip": "[0-9]{4}"}, 134 | {"abbrev": "LY", "name": "Libya"}, 135 | {"abbrev": "LI", "name": "Liechtenstein", "zip": "[0-9]{4}", "range": ["9485", "9498"]}, 136 | {"abbrev": "LT", "name": "Lithuania", "zip": "LT-[0-9]{5}"}, 137 | {"abbrev": "LU", "name": "Luxembourg", "zip": "[0-9]{4}"}, 138 | {"abbrev": "MO", "name": "Macau"}, 139 | {"abbrev": "MK", "name": "Macedonia, Republic of", "zip": "[0-9]{4}"}, 140 | {"abbrev": "MG", "name": "Madagascar", "zip": "[0-9]{3}"}, 141 | {"abbrev": "MW", "name": "Malawi"}, 142 | {"abbrev": "MY", "name": "Malaysia", "zip": "[0-9]{5}"}, 143 | {"abbrev": "MV", "name": "Maldives", "zip": "[0-9]{5}"}, 144 | {"abbrev": "ML", "name": "Mali"}, 145 | {"abbrev": "MT", "name": "Malta", "zip": "[A-Z]{3} [0-9]{4}"}, 146 | {"abbrev": "MH", "name": "Marshall Islands", "zip": "\\d{5}(?:[-\\s]\\d{4})?", "range": ["96960", "96970"]}, 147 | {"abbrev": "MQ", "name": "Martinique", "zip": "972[0-9]{2}", "range": ["97200", "97290"]}, 148 | {"abbrev": "MR", "name": "Mauritania"}, 149 | {"abbrev": "MU", "name": "Mauritius", "zip": "[0-9]{5}"}, 150 | {"abbrev": "YT", "name": "Mayotte", "zip": "976[0-9]{2}", "range": ["97600", "97690"]}, 151 | {"abbrev": "MX", "name": "Mexico", "zip": "[0-9]{5}"}, 152 | {"abbrev": "MD", "name": "Moldova, Republic of", "zip": "MD-?[0-9]{4}"}, 153 | {"abbrev": "MC", "name": "Monaco", "zip": "980[0-9]{2}"}, 154 | {"abbrev": "MN", "name": "Mongolia", "zip": "[0-9]{5}"}, 155 | {"abbrev": "ME", "name": "Montenegro", "zip": "[0-9]{5}"}, 156 | {"abbrev": "MS", "name": "Montserrat", "zip": "MSR [0-9]{4}", "range": ["MSR 1110", "MSR 1350"]}, 157 | {"abbrev": "MA", "name": "Morocco", "zip": "[0-9]{5}"}, 158 | {"abbrev": "MZ", "name": "Mozambique", "zip": "[0-9]{4}"}, 159 | {"abbrev": "MM", "name": "Myanmar", "zip": "[0-9]{5}"}, 160 | {"abbrev": "NA", "name": "Namibia"}, 161 | {"abbrev": "NR", "name": "Nauru"}, 162 | {"abbrev": "NP", "name": "Nepal", "zip": "[0-9]{5}"}, 163 | {"abbrev": "NL", "name": "Netherlands", "zip": "(?:NL-)?(\\d{4})\\s*([A-Z]{2})"}, 164 | {"abbrev": "NC", "name": "New Caledonia", "zip": "988[0-9]{2}", "range": ["96950", "96952"]}, 165 | {"abbrev": "NZ", "name": "New Zealand", "zip": "[0-9]{4}"}, 166 | {"abbrev": "NI", "name": "Nicaragua"}, 167 | {"abbrev": "NE", "name": "Niger", "zip": "[0-9]{4}"}, 168 | {"abbrev": "NG", "name": "Nigeria", "zip": "[0-9]{6}"}, 169 | {"abbrev": "NU", "name": "Niue"}, 170 | {"abbrev": "MP", "name": "Northern Mariana Islands", "zip": "^\\d{5}(?:[-\\s]\\d{4})?$"}, 171 | {"abbrev": "NO", "name": "Norway", "zip": "[0-9]{4}"}, 172 | {"abbrev": "OM", "name": "Oman", "zip": "[0-9]{3}"}, 173 | {"abbrev": "PK", "name": "Pakistan", "zip": "[0-9]{5}"}, 174 | {"abbrev": "PW", "name": "Palau", "zip": "\\d{5}(?:[-\\s]\\d{4})?"}, 175 | {"abbrev": "PA", "name": "Panama", "zip": "[0-9]{4}"}, 176 | {"abbrev": "PG", "name": "Papua New Guinea", "zip": "[0-9]{3}"}, 177 | {"abbrev": "PY", "name": "Paraguay", "zip": "[0-9]{4}"}, 178 | {"abbrev": "PE", "name": "Peru", "zip": "[0-9]{5}"}, 179 | {"abbrev": "PH", "name": "Philippines", "zip": "[0-9]{4}"}, 180 | {"abbrev": "PL", "name": "Poland", "zip": "[0-9]{2}-[0-9]{3}"}, 181 | {"abbrev": "PT", "name": "Portugal", "zip": "[0-9]{4}-[0-9]{3}"}, 182 | {"abbrev": "PR", "name": "Puerto Rico", "zip": "\\d{5}(?:[-\\s]\\d{4})?"}, 183 | {"abbrev": "QA", "name": "Qatar"}, 184 | {"abbrev": "RE", "name": "Réunion", "zip": "974[0-9]{2}", "range": ["97400", "97490"]}, 185 | {"abbrev": "RO", "name": "Romania", "zip": "[0-9]{6}"}, 186 | {"abbrev": "RU", "name": "Russian Federation", "zip": "[0-9]{6}"}, 187 | {"abbrev": "RW", "name": "Rwanda"}, 188 | {"abbrev": "MP", "name": "Saipan", "zip": "96950"}, 189 | {"abbrev": "WS", "name": "Samoa", "zip": "WS[0-9]{4}"}, 190 | {"abbrev": "ST", "name": "Sao Tome and Principe"}, 191 | {"abbrev": "SA", "name": "Saudi Arabia", "zip": "[0-9]{5}(-[0-9]{4})?"}, 192 | {"abbrev": "SN", "name": "Senegal", "zip": "[0-9]{5}"}, 193 | {"abbrev": "RS", "name": "Serbia", "zip": "[0-9]{5}"}, 194 | {"abbrev": "SC", "name": "Seychelles"}, 195 | {"abbrev": "SL", "name": "Sierra Leone"}, 196 | {"abbrev": "SG", "name": "Singapore", "zip": "[0-9]{6}"}, 197 | {"abbrev": "SK", "name": "Slovakia", "zip": "[0-9]{3} [0-9]{2}"}, 198 | {"abbrev": "SI", "name": "Slovenia", "zip": "[0-9]{4}"}, 199 | {"abbrev": "SB", "name": "Solomon Islands"}, 200 | {"abbrev": "SO", "name": "Somalia", "zip": "[A-Z]{2} [0-9]{5}"}, 201 | {"abbrev": "ZA", "name": "South Africa", "zip": "[0-9]{4}"}, 202 | {"abbrev": "SS", "name": "South Sudan"}, 203 | {"abbrev": "ES", "name": "Spain", "zip": "[0-9]{5}"}, 204 | {"abbrev": "LK", "name": "Sri Lanka", "zip": "[0-9]{4}"}, 205 | {"abbrev": "BL", "name": "St. Barthélemy", "zip": "[0-9]{5}", "range": ["97100", "97190"]}, 206 | {"abbrev": "VI", "name": "St. Croix", "zip": "[0-9]{5}"}, 207 | {"abbrev": "SE", "name": "St. Eustatius"}, 208 | {"abbrev": "SH", "name": "St. Helena", "zip": "STHL 1ZZ"}, 209 | {"abbrev": "AG", "name": "St. John", "zip": "\\d{5}(?:[-\\s]\\d{4})?"}, 210 | {"abbrev": "KN", "name": "St. Kitts and Nevis", "zip": "[A-Z]{2}[0-9]{4}"}, 211 | {"abbrev": "LC", "name": "St. Lucia", "zip": "[A-Z]{2}[0-9]{2} [0-9]{3}"}, 212 | {"abbrev": "SX", "name": "St. Maarten"}, 213 | {"abbrev": "VI", "name": "St. Thomas"}, 214 | {"abbrev": "VC", "name": "St. Vincent and the Grenadines", "zip": "VC[0-9]{4}"}, 215 | {"abbrev": "SD", "name": "Sudan", "zip": "[0-9]{5}"}, 216 | {"abbrev": "SR", "name": "Suriname"}, 217 | {"abbrev": "SZ", "name": "Swaziland", "zip": "[A-Z]{1}[0-9]{3}"}, 218 | {"abbrev": "SE", "name": "Sweden", "zip": "[0-9]{3} [0-9]{2}"}, 219 | {"abbrev": "CH", "name": "Switzerland", "zip": "[0-9]{4}"}, 220 | {"abbrev": "SY", "name": "Syria"}, 221 | {"abbrev": "PF", "name": "Tahiti", "zip": "[0-9]{5}"}, 222 | {"abbrev": "TW", "name": "Taiwan", "zip": "[0-9]{3}(-[0-9]{2})?"}, 223 | {"abbrev": "TZ", "name": "Tanzania", "zip": "[0-9]{5}"}, 224 | {"abbrev": "TH", "name": "Thailand", "zip": "[0-9]{5}"}, 225 | {"abbrev": "TG", "name": "Togo"}, 226 | {"abbrev": "TO", "name": "Tonga"}, 227 | {"abbrev": "VG", "name": "Tortola", "zip": "VG[0-9]{4}"}, 228 | {"abbrev": "TT", "name": "Trinidad and Tobago", "zip": "[0-9]{6}"}, 229 | {"abbrev": "TN", "name": "Tunisia", "zip": "[0-9]{4}"}, 230 | {"abbrev": "TR", "name": "Turkey", "zip": "[0-9]{5}"}, 231 | {"abbrev": "TM", "name": "Turkmenistan", "zip": "[0-9]{6}"}, 232 | {"abbrev": "TC", "name": "Turks and Caicos Islands", "zip": "TKCA 1ZZ"}, 233 | {"abbrev": "TV", "name": "Tuvalu"}, 234 | {"abbrev": "UG", "name": "Uganda"}, 235 | {"abbrev": "UA", "name": "Ukraine", "zip": "[0-9]{5}"}, 236 | {"abbrev": "AE", "name": "United Arab Emirates"}, 237 | { 238 | "abbrev": "GB", 239 | "name": "United Kingdom", 240 | "zip": "([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?))))\\s?[0-9][A-Za-z]{2})", 241 | }, 242 | {"abbrev": "US", "name": "United States of America", "zip": "^[0-9]{5}(?:-[0-9]{4})?$"}, 243 | {"abbrev": "UY", "name": "Uruguay", "zip": "[0-9]{5}"}, 244 | {"abbrev": "UZ", "name": "Uzbekistan", "zip": "[0-9]{6}"}, 245 | {"abbrev": "VU", "name": "Vanuatu"}, 246 | {"abbrev": "VE", "name": "Venezuela", "zip": "[0-9]{4}(-[A-Z]{1})?"}, 247 | {"abbrev": "VN", "name": "Vietnam", "zip": "[0-9]{6}"}, 248 | {"abbrev": "VG", "name": "Virgin Islands (British)", "zip": "VG[0-9]{4}"}, 249 | {"abbrev": "VI", "name": "Virgin Islands (US)", "range": ["00801", "00851"], "zip": "\\d{5}(?:[-\\s]\\d{4})?"}, 250 | {"abbrev": "YE", "name": "Yemen"}, 251 | {"abbrev": "ZM", "name": "Zambia", "zip": "[0-9]{5}"}, 252 | {"abbrev": "ZW", "name": "Zimbabwe"}, 253 | ] 254 | -------------------------------------------------------------------------------- /src/edify/library/url.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | proto = "^https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$" 4 | no_proto = "^[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$" 5 | 6 | 7 | def url(url: str, match: list = ["proto", "no_proto"]) -> bool: 8 | """Checks if a string is a valid URL. 9 | 10 | Args: 11 | url (str): The string to check. 12 | match (list): The protocols to match against. Defaults to ["https", "http", "no_proto"]. 13 | Returns: 14 | bool: True if the string is a valid URL, False otherwise. 15 | """ 16 | 17 | # Validate match argument 18 | if not isinstance(match, list): 19 | raise TypeError("match argument must be a list") 20 | 21 | if not match: 22 | raise ValueError("match argument must not be empty") 23 | 24 | # Validate protocols 25 | protocols = [] 26 | for protocol in match: 27 | if protocol == "proto": 28 | protocols.append(proto) 29 | elif protocol == "no_proto": 30 | protocols.append(no_proto) 31 | else: 32 | raise ValueError("Invalid protocol: {}".format(protocol)) 33 | 34 | # Check if URL matches any of the protocols 35 | for protocol in protocols: 36 | if re.match(protocol, url): 37 | return True 38 | 39 | return False 40 | -------------------------------------------------------------------------------- /src/edify/library/uuid.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$" 4 | 5 | 6 | def uuid(uuid: str) -> bool: 7 | """Checks if a string is a valid UUID. 8 | 9 | Args: 10 | uuid (str): The string to check. 11 | Returns: 12 | bool: True if the string is a valid UUID, False otherwise. 13 | """ 14 | return re.match(pattern, uuid) is not None 15 | -------------------------------------------------------------------------------- /src/edify/library/zip.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .support.zip import ZIP_LOCALES 4 | 5 | locales = [locale["abbrev"] for locale in ZIP_LOCALES] 6 | 7 | 8 | def zip(zip: str, locale: str = "US") -> bool: 9 | """Check if a string is a valid zip code. 10 | 11 | Args: 12 | zip (str): The string to check. 13 | locale (str): (optional) The locale to check against. Defaults to "US". 14 | Returns: 15 | bool: True if the string is a valid zip code, False otherwise. 16 | """ 17 | 18 | if not isinstance(locale, str): 19 | raise TypeError("locale must be a string") 20 | 21 | if locale == "": 22 | raise ValueError("locale cannot be empty") 23 | 24 | if locale not in locales: 25 | print(locales) 26 | raise ValueError("locale must be one of {}".format(locales)) 27 | 28 | pattern = ZIP_LOCALES[locales.index(locale)]["zip"] 29 | return re.match(pattern, zip) is not None 30 | -------------------------------------------------------------------------------- /tests.local.sh: -------------------------------------------------------------------------------- 1 | # Clean tox environment 2 | tox -e clean 3 | 4 | # Sort imports 5 | isort . 6 | 7 | # Run Tests 8 | tox -e check -v 9 | 10 | # Run Docs 11 | tox -e docs -v 12 | 13 | # Get the current installed python version 14 | PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') 15 | 16 | # Subset the python version to the major.minor version 17 | PYTHON_VERSION=$(echo $PYTHON_VERSION | cut -d. -f1,2) 18 | 19 | if [ "$PYTHON_VERSION" = "3.7" ]; then 20 | # Build using python 3.7 21 | tox -e py37 -v 22 | elif [ "$PYTHON_VERSION" = "3.8" ]; then 23 | # Build using python 3.8 24 | tox -e py38 -v 25 | elif [ "$PYTHON_VERSION" = "3.9" ]; then 26 | # Build using python 3.9 27 | tox -e py39 -v 28 | elif [ "$PYTHON_VERSION" = "3.10" ]; then 29 | # Build using python 3.10 30 | tox -e py310 -v 31 | elif [ "$PYTHON_VERSION" = "3.11" ]; then 32 | # Build using python 3.11 33 | tox -e py311 -v 34 | else 35 | # Show error message 36 | echo "Python version $PYTHON_VERSION is not supported" 37 | fi 38 | 39 | # Run Coverage 40 | tox -e report -v 41 | -------------------------------------------------------------------------------- /tests/ssn_test.py: -------------------------------------------------------------------------------- 1 | from edify.library import ssn 2 | 3 | 4 | def test_ssn(): 5 | ssns = { 6 | "000-22-3333": False, 7 | "100-22-3333": True, 8 | "": False, 9 | 123: False, 10 | } 11 | for s_s_n, expected in ssns.items(): 12 | assert ssn(s_s_n) == expected 13 | -------------------------------------------------------------------------------- /tests/test_builder.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from edify import RegexBuilder 4 | 5 | simple_se = RegexBuilder().string('hello').any_char().string('world') 6 | flags_se = RegexBuilder().multi_line().ignore_case().string('hello').any_char().string('world') 7 | start_end_se = RegexBuilder().start_of_input().string('hello').any_char().string('world').end_of_input() 8 | nc_se = RegexBuilder().named_capture('module').exactly(2).any_char().end().named_back_reference('module') 9 | indexed_back_reference_se = RegexBuilder().capture().exactly(2).any_char().end().back_reference(1) 10 | nested_se = RegexBuilder().exactly(2).any_char() 11 | first_layer_se = ( 12 | RegexBuilder().string('outer begin').named_capture('inner_subexpression').optional().subexpression(nested_se).end().string('outer end') 13 | ) 14 | 15 | 16 | def regex_equality(regex, rb_expression): 17 | regex_str = str(regex) 18 | rb_expression_str = rb_expression.to_regex_string() 19 | assert regex_str == str(rb_expression_str) 20 | 21 | 22 | def regex_compilation(regex, rb_expression, f=0): 23 | rb_expression_c = rb_expression.to_regex() 24 | assert re.compile(regex, flags=f) == rb_expression_c 25 | 26 | 27 | def test_empty_regex(): 28 | expr = RegexBuilder() 29 | regex_equality('/(?:)/', expr) 30 | regex_compilation('(?:)', expr) 31 | 32 | 33 | def test_flag_a(): 34 | expr = RegexBuilder().ascii_only() 35 | regex_equality('/(?:)/A', expr) 36 | regex_compilation('(?:)', expr, re.A) 37 | 38 | 39 | def test_flag_d(): 40 | expr = RegexBuilder().debug() 41 | regex_equality('/(?:)/D', expr) 42 | regex_compilation('(?:)', expr, re.DEBUG) 43 | 44 | 45 | def test_flag_i(): 46 | expr = RegexBuilder().ignore_case() 47 | regex_equality('/(?:)/I', expr) 48 | regex_compilation('(?:)', expr, re.I) 49 | 50 | 51 | def test_flag_m(): 52 | expr = RegexBuilder().multi_line() 53 | regex_equality('/(?:)/M', expr) 54 | regex_compilation('(?:)', expr, re.M) 55 | 56 | 57 | def test_flag_s(): 58 | expr = RegexBuilder().dot_all() 59 | regex_equality('/(?:)/S', expr) 60 | regex_compilation('(?:)', expr, re.S) 61 | 62 | 63 | def test_flag_x(): 64 | expr = RegexBuilder().verbose() 65 | regex_equality('/(?:)/X', expr) 66 | regex_compilation('(?:)', expr, re.X) 67 | 68 | 69 | def test_any_char(): 70 | expr = RegexBuilder().any_char() 71 | regex_equality('/./', expr) 72 | regex_compilation('.', expr) 73 | 74 | 75 | def test_whitespace_char(): 76 | expr = RegexBuilder().whitespace_char() 77 | regex_equality('/\\s/', expr) 78 | regex_compilation('\\s', expr) 79 | 80 | 81 | def test_non_whitespace_char(): 82 | expr = RegexBuilder().non_whitespace_char() 83 | regex_equality('/\\S/', expr) 84 | regex_compilation('\\S', expr) 85 | 86 | 87 | def test_digit(): 88 | expr = RegexBuilder().digit() 89 | regex_equality('/\\d/', expr) 90 | regex_compilation('\\d', expr) 91 | 92 | 93 | def test_non_digit(): 94 | expr = RegexBuilder().non_digit() 95 | regex_equality('/\\D/', expr) 96 | regex_compilation('\\D', expr) 97 | 98 | 99 | def test_word(): 100 | expr = RegexBuilder().word() 101 | regex_equality('/\\w/', expr) 102 | regex_compilation('\\w', expr) 103 | 104 | 105 | def test_non_word(): 106 | expr = RegexBuilder().non_word() 107 | regex_equality('/\\W/', expr) 108 | regex_compilation('\\W', expr) 109 | 110 | 111 | def test_word_boundary(): 112 | expr = RegexBuilder().word_boundary() 113 | regex_equality('/\\b/', expr) 114 | regex_compilation('\\b', expr) 115 | 116 | 117 | def test_non_word_boundary(): 118 | expr = RegexBuilder().non_word_boundary() 119 | regex_equality('/\\B/', expr) 120 | regex_compilation('\\B', expr) 121 | 122 | 123 | def test_new_line(): 124 | expr = RegexBuilder().new_line() 125 | regex_equality('/\\n/', expr) 126 | regex_compilation('\\n', expr) 127 | 128 | 129 | def test_carriage_return(): 130 | expr = RegexBuilder().carriage_return() 131 | regex_equality('/\\r/', expr) 132 | regex_compilation('\\r', expr) 133 | 134 | 135 | def test_tab(): 136 | expr = RegexBuilder().tab() 137 | regex_equality('/\\t/', expr) 138 | regex_compilation('\\t', expr) 139 | 140 | 141 | def test_null_byte(): 142 | expr = RegexBuilder().null_byte() 143 | regex_equality('/\\0/', expr) 144 | regex_compilation('\\0', expr) 145 | 146 | 147 | def test_any_of_basic(): 148 | expr = RegexBuilder().any_of().string('hello').digit().word().char('.').char('#').end() 149 | regex_equality('/(?:hello|\\d|\\w|[\\.\\#])/', expr) 150 | regex_compilation('(?:hello|\\d|\\w|[\\.\\#])', expr) 151 | 152 | 153 | def test_any_of_range_fusion(): 154 | expr = RegexBuilder().any_of().range('a', 'z').range('A', 'Z').range('0', '9').char('.').char('#').end() 155 | regex_equality('/[a-zA-Z0-9\\.\\#]/', expr) 156 | regex_compilation('[a-zA-Z0-9\\.\\#]', expr) 157 | 158 | 159 | def test_any_of_range_fusion_with_other_choices(): 160 | expr = RegexBuilder().any_of().range('a', 'z').range('A', 'Z').range('0', '9').char('.').char('#').string('hello').end() 161 | regex_equality('/(?:hello|[a-zA-Z0-9\\.\\#])/', expr) 162 | regex_compilation('(?:hello|[a-zA-Z0-9\\.\\#])', expr) 163 | 164 | 165 | def test_capture(): 166 | expr = RegexBuilder().capture().string('hello ').word().char('!').end() 167 | regex_equality('/(hello \\w!)/', expr) 168 | regex_compilation('(hello \\w!)', expr) 169 | 170 | 171 | def test_named_capture(): 172 | expr = RegexBuilder().named_capture('this_is_the_name').string('hello ').word().char('!').end() 173 | regex_equality('/(?Phello \\w!)/', expr) 174 | regex_compilation('(?Phello \\w!)', expr) 175 | 176 | 177 | def test_bad_name_error(): 178 | try: 179 | (RegexBuilder().named_capture('hello world').string('hello ').word().char('!').end()) 180 | except Exception as e: 181 | assert isinstance(e, Exception) 182 | 183 | 184 | def test_same_name_error(): 185 | try: 186 | ( 187 | RegexBuilder() 188 | .namedCapture('hello') 189 | .string('hello ') 190 | .word() 191 | .char('!') 192 | .end() 193 | .namedCapture('hello') 194 | .string('hello ') 195 | .word() 196 | .char('!') 197 | .end() 198 | ) 199 | except Exception as e: 200 | assert isinstance(e, Exception) 201 | 202 | 203 | def test_named_back_reference(): 204 | expr = RegexBuilder().named_capture('this_is_the_name').string('hello ').word().char('!').end().named_back_reference('this_is_the_name') 205 | regex_equality('/(?Phello \\w!)\\k/', expr) 206 | # Python does not support named back references, so we raise an error 207 | try: 208 | expr.to_regex() 209 | except Exception as e: 210 | assert isinstance(e, Exception) 211 | 212 | 213 | def test_named_back_reference_no_cg_exists(): 214 | try: 215 | RegexBuilder().named_back_reference('not_here') 216 | except Exception as e: 217 | assert isinstance(e, Exception) 218 | 219 | 220 | def test_back_reference(): 221 | expr = RegexBuilder().capture().string('hello ').word().char('!').end().back_reference(1) 222 | regex_equality('/(hello \\w!)\\1/', expr) 223 | regex_compilation('(hello \\w!)\\1', expr) 224 | 225 | 226 | def test_back_reference_no_cg_exists(): 227 | try: 228 | RegexBuilder().back_reference(1) 229 | except Exception as e: 230 | assert isinstance(e, Exception) 231 | 232 | 233 | def test_group(): 234 | expr = RegexBuilder().group().string('hello ').word().char('!').end() 235 | regex_equality('/(?:hello \\w!)/', expr) 236 | regex_compilation('(?:hello \\w!)', expr) 237 | 238 | 239 | def test_error_when_called_with_no_stack(): 240 | try: 241 | RegexBuilder().end() 242 | except Exception as e: 243 | assert isinstance(e, Exception) 244 | 245 | 246 | def test_assert_ahead(): 247 | expr = RegexBuilder().assert_ahead().range('a', 'f').end().range('a', 'z') 248 | regex_equality('/(?=[a-f])[a-z]/', expr) 249 | regex_compilation('(?=[a-f])[a-z]', expr) 250 | 251 | 252 | def test_assert_behind(): 253 | expr = RegexBuilder().assert_behind().string('hello ').end().range('a', 'z') 254 | regex_equality('/(?<=hello )[a-z]/', expr) 255 | regex_compilation('(?<=hello )[a-z]', expr) 256 | 257 | 258 | def test_assert_not_ahead(): 259 | expr = RegexBuilder().assert_not_ahead().range('a', 'f').end().range('0', '9') 260 | regex_equality('/(?![a-f])[0-9]/', expr) 261 | regex_compilation('(?![a-f])[0-9]', expr) 262 | 263 | 264 | def test_assert_not_behind(): 265 | expr = RegexBuilder().assert_not_behind().string('hello ').end().range('a', 'z') 266 | regex_equality('/(?.{2})\\k[0-9]/', expr) 480 | try: 481 | expr.to_regex() 482 | except Exception as e: 483 | assert isinstance(e, Exception) 484 | 485 | 486 | def test_namespacing(): 487 | expr = RegexBuilder().at_least(3).digit().subexpression(nc_se, {'namespace': 'yolo'}).range('0', '9') 488 | regex_equality('/\\d{3,}(?P.{2})\\k[0-9]/', expr) 489 | try: 490 | expr.to_regex() 491 | except Exception as e: 492 | assert isinstance(e, Exception) 493 | 494 | 495 | def test_group_name_collision_error(): 496 | try: 497 | (RegexBuilder().namedCapture('module').at_least(3).digit().end().subexpression(nc_se).range('0', '9')) 498 | except Exception as e: 499 | assert isinstance(e, Exception) 500 | 501 | 502 | def test_group_name_collision_error_after_namespacing(): 503 | try: 504 | (RegexBuilder().namedCapture('module').at_least(3).digit().end().subexpression(nc_se, {'namespace': 'yolo'}).range('0', '9')) 505 | except Exception as e: 506 | assert isinstance(e, Exception) 507 | 508 | 509 | def test_indexed_back_referencing(): 510 | expr = RegexBuilder().capture().at_least(3).digit().end().subexpression(indexed_back_reference_se).back_reference(1).range('0', '9') 511 | regex_equality('/(\\d{3,})(.{2})\\2\\1[0-9]/', expr) 512 | regex_compilation('(\\d{3,})(.{2})\\2\\1[0-9]', expr) 513 | 514 | 515 | def test_deeply_nested_se(): 516 | expr = RegexBuilder().capture().at_least(3).digit().end().subexpression(first_layer_se).back_reference(1).range('0', '9') 517 | regex_equality('/(\\d{3,})outer begin(?P(?:.{2})?)outer end\\1[0-9]/', expr) 518 | regex_compilation('(\\d{3,})outer begin(?P(?:.{2})?)outer end\\1[0-9]', expr) 519 | -------------------------------------------------------------------------------- /tests/test_date.py: -------------------------------------------------------------------------------- 1 | from edify.library import date 2 | from edify.library import iso_date 3 | 4 | 5 | def test_date(): 6 | dates = { 7 | "1/1/2020": True, 8 | "01/01/2020": True, 9 | "1/01/2020": True, 10 | "01/1/2020": True, 11 | "1/1/20": False, 12 | "01/01/20": False, 13 | "1/1/202": False, 14 | "01/01/202": False, 15 | "12/12/2022": True, 16 | "12/12/2": False, 17 | "2021-11-04T22:32:47.142354-10:00": False, 18 | "2021-11-04T22:32:47.142354Z": False, 19 | "2021-11-04T22:32:47.142354": False, 20 | "2021-11-04T22:32:47": False, 21 | "2021-11-04T22:32": False, 22 | "2021-11-04T22": False, 23 | "2021-11-04": False, 24 | "2021-11": False, 25 | "2021": False, 26 | "1-1-2020": False 27 | } 28 | 29 | for date_string, expectation in dates.items(): 30 | assert date(date_string) == expectation 31 | 32 | 33 | def test_iso_date(): 34 | dates = { 35 | "1/1/2020": False, 36 | "01/01/2020": False, 37 | "1/01/2020": False, 38 | "01/1/2020": False, 39 | "1/1/20": False, 40 | "01/01/20": False, 41 | "1/1/202": False, 42 | "01/01/202": False, 43 | "12/12/2022": False, 44 | "12/12/2": False, 45 | "2021-11-04T22:32:47.142354-10:00": True, 46 | "2021-11-04T22:32:47.142354Z": True, 47 | "2021-11-04T22:32:47.142354": True, 48 | "2021-11-04T22:32:47": True, 49 | "2021-11-04T22:32": False, 50 | "2021-11-04T22": False, 51 | "2021-11-04": False, 52 | "2021-11": False, 53 | "2021": False, 54 | } 55 | 56 | for date_string, expectation in dates.items(): 57 | assert iso_date(date_string) == expectation 58 | -------------------------------------------------------------------------------- /tests/test_email.py: -------------------------------------------------------------------------------- 1 | from edify.library import email 2 | from edify.library import email_rfc_5322 3 | 4 | emails = [ 5 | "email@example.com", 6 | "email@192.168.0.1", 7 | "firstname.lastname@example.com", 8 | "email@subdomain.example.com", 9 | "firstname+lastname@example.com", 10 | "1234567890@example.com", 11 | "email@example-one.com", 12 | "_______@example.com", 13 | "email@example.museum", 14 | "firstname-lastname@example.com", 15 | "email@example.com.", 16 | "plainaddress", 17 | "#@%^%#$@#$@#.com", 18 | "@example.com", 19 | "Joe Smith ", 20 | "email.example.com", 21 | "email@example@example.com", 22 | ".email@example.com", 23 | "email.@example.com", 24 | "email..email@example.com", 25 | "あいうえお@example.com", 26 | "email@-example.com", 27 | "Abc..123@example.com" 28 | ] 29 | 30 | 31 | def test_email(): 32 | 33 | expectations = [ 34 | True, 35 | True, 36 | True, 37 | True, 38 | True, 39 | True, 40 | True, 41 | True, 42 | True, 43 | True, 44 | False, 45 | False, 46 | False, 47 | False, 48 | False, 49 | False, 50 | False, 51 | False, 52 | False, 53 | False, 54 | False, 55 | False, 56 | False 57 | ] 58 | for i in range(len(emails)): 59 | assert email(emails[i]) == expectations[i] 60 | 61 | 62 | def test_email_rfc_5322(): 63 | expectations = [ 64 | True, 65 | True, 66 | True, 67 | True, 68 | True, 69 | True, 70 | True, 71 | True, 72 | True, 73 | True, 74 | True, 75 | False, 76 | False, 77 | False, 78 | False, 79 | False, 80 | False, 81 | False, 82 | False, 83 | False, 84 | False, 85 | False, 86 | False 87 | ] 88 | for i in range(len(emails)): 89 | assert email_rfc_5322(emails[i]) == expectations[i] 90 | -------------------------------------------------------------------------------- /tests/test_guid.py: -------------------------------------------------------------------------------- 1 | from edify.library import guid 2 | 3 | 4 | def test_valid_guids(): 5 | guids = { 6 | "6ba7b810-9dad-11d1-80b4-00c04fd430c8": True, 7 | '{51d52cf1-83c9-4f02-b117-703ecb728b74}': True, 8 | '{51d52cf1-83c9-4f02-b117-703ecb728-b74}': False, 9 | } 10 | for guid_string, expectation in guids.items(): 11 | assert guid(guid_string) == expectation 12 | -------------------------------------------------------------------------------- /tests/test_ip.py: -------------------------------------------------------------------------------- 1 | from edify.library import ipv4 2 | from edify.library import ipv6 3 | 4 | # Generate ipv4 dictionary 5 | ipv4_dict = { 6 | "192.168.0.1": True, 7 | "244.232.123.233": True, 8 | "363.232.123.233": False, 9 | "234.234234.234.234": False, 10 | "12.12.12.12.12": False, 11 | "0.0.0.0": True, 12 | "987.987.987.987": False, 13 | } 14 | 15 | # Generate ipv6 dictionary 16 | ipv6_dict = { 17 | "2001:0db8:85a3:0000:0000:8a2e:0370:7334": True, 18 | "2001:db8:85a3:0:0:8a2e:370:7334": True, 19 | "2001:db8:85a3::8a2e:370:7334": True, 20 | "2001:db8:85a3:0:0:8A2E:370:7334": True, 21 | "2001:db8:85a3:0:0:8a2e:370:7334:": False, 22 | "2001:db8:85a3:0:0:8a2e:370:7334:7334": False, 23 | "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334": False, 24 | "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334:7334": False, 25 | } 26 | 27 | 28 | def test_ipv4(): 29 | for ip, expectation in ipv4_dict.items(): 30 | assert ipv4(ip) == expectation 31 | 32 | 33 | def test_ipv6(): 34 | for ip, expectation in ipv6_dict.items(): 35 | assert ipv6(ip) == expectation 36 | -------------------------------------------------------------------------------- /tests/test_mac.py: -------------------------------------------------------------------------------- 1 | from edify.library import mac 2 | 3 | 4 | def test_mac(): 5 | macs = { 6 | "00:00:5e:00:53:af": True, 7 | "00:00:5e:00:53:af:": False, 8 | 123: False, 9 | } 10 | 11 | for m_a_c, expected in macs.items(): 12 | assert mac(m_a_c) == expected 13 | -------------------------------------------------------------------------------- /tests/test_password.py: -------------------------------------------------------------------------------- 1 | from edify.library import password 2 | 3 | 4 | def test_password(): 5 | assert password("password") is False 6 | assert password("Password123!") is True 7 | assert password("Password123!", max_length=8) is False 8 | assert password("Password123!", min_upper=2) is False 9 | assert password("password", min_upper=0, min_digit=0, min_special=0) is True 10 | assert password("pass@#1", min_special=1, special_chars="!", min_digit=0, min_upper=0, min_length=4) is False 11 | -------------------------------------------------------------------------------- /tests/test_phone.py: -------------------------------------------------------------------------------- 1 | from edify.library import phone_number 2 | 3 | 4 | def test(): 5 | phones = { 6 | "1234567890": True, 7 | "123 456 7890": True, 8 | "123-456-7890": True, 9 | "123.456.7890": True, 10 | "123 456 7890": True, 11 | "+1 (123) 456-7890": True, 12 | "+1 (123) 456 7890": True, 13 | "+1-(123)-456-7890": True, 14 | "+102 (123) 456-7890": True, 15 | "+91 (123) 456-7890": True, 16 | "90122121": True, 17 | "12345678901": True, 18 | "+1 (124) 232": True, 19 | "+1 (123) 45-890": True, 20 | "+1 (1) 456-7890": True, 21 | "9012": True, 22 | "911": True, 23 | "+1 (615) 243-": False 24 | } 25 | for phone, expectation in phones.items(): 26 | assert phone_number(phone) == expectation 27 | -------------------------------------------------------------------------------- /tests/test_url.py: -------------------------------------------------------------------------------- 1 | from edify.library import url 2 | 3 | urls = [ 4 | "example.com", 5 | "www.example.com", 6 | "www.example.com/path/to/file", 7 | "http://www.example.com", 8 | "http://example.com", 9 | "http://www.example.com/path/to/page", 10 | "https://example.com", 11 | "https://www.example.com/", 12 | "https://www.example.com/path/to/page", 13 | "//example.com", 14 | ] 15 | 16 | 17 | def test_all_protocols(): 18 | match_list = ["proto", "no_proto"] 19 | expected = [True] * 9 + [False] 20 | for uri, expectation in zip(urls, expected): 21 | assert url(uri, match=match_list) == expectation 22 | 23 | 24 | def test_proto_only(): 25 | match_list = ["proto"] 26 | expected = [False] * 3 + [True] * 6 + [False] 27 | for uri, expectation in zip(urls, expected): 28 | print(uri, expectation) 29 | assert url(uri, match=match_list) == expectation 30 | 31 | 32 | def test_no_proto_only(): 33 | match_list = ["no_proto"] 34 | expected = [True] * 3 + [False] * 7 35 | for uri, expectation in zip(urls, expected): 36 | assert url(uri, match=match_list) == expectation 37 | 38 | 39 | def test_invalid_protocol(): 40 | match_list = ["invalid"] 41 | for uri in urls: 42 | try: 43 | url(uri, match=match_list) 44 | except ValueError: 45 | assert True 46 | 47 | 48 | def test_invalid_match_type(): 49 | match_list = "invalid" 50 | for uri in urls: 51 | try: 52 | url(uri, match=match_list) 53 | except TypeError: 54 | assert True 55 | 56 | 57 | def test_empty_match_list(): 58 | match_list = [] 59 | for uri in urls: 60 | try: 61 | url(uri, match=match_list) 62 | except ValueError: 63 | assert True 64 | -------------------------------------------------------------------------------- /tests/test_uuid.py: -------------------------------------------------------------------------------- 1 | from edify.library import uuid 2 | 3 | uuids = { 4 | "123e4567-e89b-12d3-a456-426614174000": True, 5 | "123e456-789b-12d3-426614174000": False, 6 | "123e456-789b-12d3-a456-426614174000-12ad3r": False, 7 | "123e456": False, 8 | } 9 | 10 | 11 | def test_valid_uuids(): 12 | for uuid_string, expectation in uuids.items(): 13 | assert uuid(uuid_string) == expectation 14 | -------------------------------------------------------------------------------- /tests/test_zip.py: -------------------------------------------------------------------------------- 1 | from edify.library import zip 2 | 3 | 4 | def test_valid_zips(): 5 | zips = {"12345": True, "12345-1234": True, "12345-123456": False, "1234": False} 6 | for zip_string, expectation in zips.items(): 7 | assert zip(zip_string) == expectation 8 | 9 | 10 | def test_invalid_locale(): 11 | try: 12 | zip("12345", locale="INVALID") 13 | except ValueError: 14 | assert True 15 | 16 | 17 | def test_invalid_locale_type(): 18 | try: 19 | zip("12345", 5) 20 | except TypeError: 21 | assert True 22 | 23 | 24 | def test_empty_locale(): 25 | try: 26 | zip("12345", "") 27 | except ValueError: 28 | assert True 29 | 30 | 31 | def test_locale_IN(): 32 | zips = {"123456": True, "000000": False, "012345": False, "12345": False, "1234567": False} 33 | for zip_string, expectation in zips.items(): 34 | assert zip(zip_string, locale="IN") == expectation 35 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv:bootstrap] 2 | deps = 3 | jinja2 4 | tox 5 | skip_install = true 6 | commands = 7 | python ci/bootstrap.py --no-env 8 | passenv = 9 | * 10 | ; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist 11 | 12 | [tox] 13 | envlist = 14 | clean, 15 | check, 16 | docs, 17 | {py37,py38,py39,py310,py311,pypy37,pypy38}, 18 | report 19 | ignore_basepython_conflict = true 20 | 21 | [testenv] 22 | basepython = 23 | pypy37: {env:TOXPYTHON:pypy3.7} 24 | pypy38: {env:TOXPYTHON:pypy3.8} 25 | py37: {env:TOXPYTHON:python3.7} 26 | py38: {env:TOXPYTHON:python3.8} 27 | py39: {env:TOXPYTHON:python3.9} 28 | py310: {env:TOXPYTHON:python3.10} 29 | py311: {env:TOXPYTHON:python3.11} 30 | {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3} 31 | setenv = 32 | PYTHONPATH={toxinidir}/tests 33 | PYTHONUNBUFFERED=yes 34 | passenv = 35 | * 36 | usedevelop = false 37 | deps = 38 | pytest 39 | pytest-cov 40 | commands = 41 | {posargs:pytest --cov --cov-report=term-missing -vv tests} 42 | 43 | [testenv:check] 44 | deps = 45 | docutils 46 | check-manifest 47 | flake8 48 | readme-renderer 49 | pygments 50 | isort 51 | skip_install = true 52 | commands = 53 | python setup.py check --strict --metadata --restructuredtext 54 | check-manifest {toxinidir} 55 | flake8 56 | isort --verbose --check-only --diff --filter-files . 57 | 58 | [testenv:docs] 59 | usedevelop = true 60 | deps = 61 | -r{toxinidir}/docs/requirements.txt 62 | commands = 63 | sphinx-build {posargs:-E} -b html docs dist/docs 64 | sphinx-build -b linkcheck docs dist/docs 65 | 66 | [testenv:codecov] 67 | deps = 68 | codecov 69 | skip_install = true 70 | commands = 71 | codecov [] 72 | 73 | [testenv:report] 74 | deps = 75 | coverage 76 | skip_install = true 77 | commands = 78 | coverage report 79 | coverage html 80 | 81 | [testenv:clean] 82 | commands = coverage erase 83 | skip_install = true 84 | deps = 85 | coverage 86 | --------------------------------------------------------------------------------