Repository: brazilian-utils/brutils-python (2023-07-23 ~ 2023-10-05)

problem_statement: Remover SÃ­mbolos de CEPs
Implementar um utilitÃ¡rio que recebe um CEP com sÃ­mbolos (ponto e traÃ§o) e retorna esse CEP apenas com dÃ­gitos
date: 2023-07-23
patch: diff --git a/brutils/__init__.py b/brutils/__init__.py
index e51068c..839548a 100644
--- a/brutils/__init__.py
+++ b/brutils/__init__.py
@@ -14,4 +14,5 @@ from brutils.cep import (
     is_valid as is_valid_cep,
     format_cep,
     generate as generate_cep,
+    remove_symbols as remove_symbols_cep,
 )
diff --git a/brutils/cep.py b/brutils/cep.py
index cd86408..ecf67a0 100644
--- a/brutils/cep.py
+++ b/brutils/cep.py
@@ -5,6 +5,16 @@ from random import randint
 ############
 
 
+def remove_symbols(dirty):  # type: (str) -> str
+    """
+    Filters out CEP formatting symbols. Symbols that are not used
+    in the CEP formatting are left unfiltered on purpose so that
+    if fails other tests, because their presence indicate that the
+    input was somehow corrupted.
+    """
+    return "".join(filter(lambda char: char not in ".-", dirty))
+
+
 def format_cep(cep):  # type: (str) -> str
     """
     Will format an adequately formatted numbers-only CEP string,

test_patch: diff --git a/tests/test_cep.py b/tests/test_cep.py
index a2a2eca..56b12a6 100644
--- a/tests/test_cep.py
+++ b/tests/test_cep.py
@@ -13,11 +13,22 @@ from brutils.cep import (
     is_valid,
     format_cep,
     generate,
+    remove_symbols,
 )
 from unittest import TestCase, main
 
 
 class CEP(TestCase):
+    def test_remove_symbols(self):
+        assert remove_symbols("00000000") == "00000000"
+        assert remove_symbols("01310-200") == "01310200"
+        assert remove_symbols("01..310.-200.-") == "01310200"
+        assert remove_symbols("abc01310200*!*&#") == "abc01310200*!*&#"
+        assert (
+            remove_symbols("ab.c1.--.3-102.-0-.0-.*.-!*&#") == "abc1310200*!*&#"
+        )
+        assert remove_symbols("...---...") == ""
+
     def test_format_cep(self):
         with patch("brutils.cep.is_valid", return_value=True) as mock_is_valid:
             # When cep is_valid, returns formatted cep


----

problem_statement: Validar PIS
**Seu pedido de recurso estÃ¡ relacionado a um problema? Por favor, descreva.**
Verificar se uma String corresponde a um nÃºmero de PIS vÃ¡lido, conforme as regras da numeraÃ§Ã£o.  

**Descreva a soluÃ§Ã£o que vocÃª gostaria**
Chamar um novo utilitÃ¡rio passando uma String e receber um boolean indicando se a String Ã© um nÃºmero de PIS vÃ¡lido ou nÃ£o.

**Descreva alternativas que vocÃª considerou**
Criar um novo utilitÃ¡rio `is_valid_pis`.
```python
def is_valid_pis(pis):
...
```
Exemplo de uso:
```python
from brutils import is_valid_pis

>>> is_valid_pis("12062828707")
True
>>> is_valid_pis("12066181422")
True
>>> is_valid_pis("12066181427")
False
>>> is_valid_pis("1206282")
False
```

**Contexto adicional**
- NÃ£o Ã© necessÃ¡rio validar se o PIS existe de fato;
- https://pt.wikipedia.org/wiki/PIS/PASEP
- https://www.macoratti.net/alg_pis.htm
- https://www.geradorpis.com/
date: 2023-10-05
patch: diff --git a/brutils/__init__.py b/brutils/__init__.py
index 839548a..8bc0b0d 100644
--- a/brutils/__init__.py
+++ b/brutils/__init__.py
@@ -16,3 +16,20 @@ from brutils.cep import (
     generate as generate_cep,
     remove_symbols as remove_symbols_cep,
 )
+from brutils.phone import (
+    is_valid_landline as is_valid_landline_phone,
+    is_valid_mobile as is_valid_mobile_phone,
+    is_valid as is_valid_phone,
+)
+
+from brutils.license_plate import (
+    is_valid_mercosul as is_valid_license_plate_mercosul,
+)
+
+from brutils.email import is_valid as is_valid_email
+
+from brutils.license_plate import is_valid_license_plate_old_format
+
+from brutils.pis import (
+    is_valid as is_valid_pis,
+)
diff --git a/brutils/email.py b/brutils/email.py
new file mode 100644
index 0000000..6fce73c
--- /dev/null
+++ b/brutils/email.py
@@ -0,0 +1,17 @@
+import re
+
+
+def is_valid(email):  # type: (str) -> bool
+    """
+    Check if a string corresponds to a valid email address.
+
+    Args:
+        email (str): The input string to be checked.
+
+    Returns:
+        bool: True if email is a valid email address, False otherwise.
+    """
+    pattern = re.compile(
+        r"^(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
+    )
+    return isinstance(email, str) and re.match(pattern, email) is not None
diff --git a/brutils/license_plate.py b/brutils/license_plate.py
new file mode 100644
index 0000000..5c598c7
--- /dev/null
+++ b/brutils/license_plate.py
@@ -0,0 +1,28 @@
+import re
+
+
+def is_valid_license_plate_old_format(plate: str) -> bool:
+    """
+    Checks whether a string matches the old format of Brazilian license plate.
+    """
+    pattern = re.compile(r"^[aA-zZ]{3}[0-9]{4}$")
+    return (
+        isinstance(plate, str) and re.match(pattern, plate.strip()) is not None
+    )
+
+
+import re
+
+
+def is_valid_mercosul(license_plate):  # type: (str) -> bool
+    """
+    Returns whether or not the provided license_plate is valid according to the
+    Mercosul pattern (LLLNLNN). Input should be a digit string of proper
+    length. Ex: ABC4E67
+    """
+    if not isinstance(license_plate, str):
+        return False
+
+    license_plate = license_plate.upper().strip()
+    pattern = re.compile(r"^[A-Z]{3}\d[A-Z]\d{2}$")
+    return re.match(pattern, license_plate) is not None
diff --git a/brutils/phone.py b/brutils/phone.py
new file mode 100644
index 0000000..0d161d7
--- /dev/null
+++ b/brutils/phone.py
@@ -0,0 +1,64 @@
+import re
+
+# FORMATTING
+############
+
+
+# OPERATIONS
+############
+
+
+def is_valid_landline(phone_number):  # type: (str) -> bool
+    """
+    Returns whether or not the verifying first 3 digits are a
+    match.This function validates only Brazilian landline numbers
+    and does not verify if the number actually exists.
+    Input should be a digit string of proper length.
+
+    """
+    pattern = re.compile(r"^[1-9][1-9][2-5]\d{7}$")
+    return (
+        isinstance(phone_number, str)
+        and re.match(pattern, phone_number) is not None
+    )
+
+
+def is_valid_mobile(phone_number):  # type: (str) -> bool
+    """
+    Returns whether or not the verifying first 3 digits are a
+    match.This function validates only Brazilian mobile numbers
+    and does not verify if the number actually exists.
+    Input should be a digit string of proper length.
+
+    """
+    pattern = re.compile(r"^[1-9][1-9][9]\d{8}$")
+    return (
+        isinstance(phone_number, str)
+        and re.match(pattern, phone_number) is not None
+    )
+
+
+def is_valid(phone_number):  # type: (str) -> bool
+    """
+    Returns whether or not the verifying first 3 digits are a
+    match.This function validates only Brazilian phone numbers
+    and does not verify if the number actually exists.
+    Input should be a digit string of proper length.
+
+    """
+    return is_valid_landline(phone_number) or is_valid_mobile(phone_number)
+
+
+def remove_symbols_phone(phone_number):  # type: (str) -> str
+    """
+    Removes common symbols from a Brazilian phone number string.
+
+    """
+    cleaned_phone = (
+        phone_number.replace("(", "")
+        .replace(")", "")
+        .replace("-", "")
+        .replace("+", "")
+        .replace(" ", "")
+    )
+    return cleaned_phone
diff --git a/brutils/pis.py b/brutils/pis.py
new file mode 100644
index 0000000..ddb213b
--- /dev/null
+++ b/brutils/pis.py
@@ -0,0 +1,57 @@
+WEIGHTS = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
+
+
+# OPERATIONS
+############
+
+
+def validate(pis: str) -> bool:
+    """
+    Validate a Brazilian PIS number.
+
+    The PIS is valid if:
+    - It has 11 digits
+    - All characters are digits
+    - It passes the weight calculation check
+
+    Args:
+        pis[str]: PIS number as a string.
+
+    Returns:
+        value[bool]: True if PIS is valid, False otherwise.
+    """
+    if len(pis) != 11 or not pis.isdigit():
+        return False
+
+    return pis[-1] == str(_checksum(pis[:-1]))
+
+
+def is_valid(pis: str) -> bool:
+    """
+    Returns whether or not the verifying checksum digit of the
+    given `pis` match its base number.
+
+    Args:
+        pis[str]: PIS number as a string of proper length.
+
+    Returns:
+        value[bool]: True if PIS is valid, False otherwise.
+    """
+    return isinstance(pis, str) and validate(pis)
+
+
+def _checksum(base_pis: str) -> int:
+    """
+    Calculates the checksum digit of the given `base_pis` string.
+
+    Args:
+        base_pis [str]: 10 first digits of PIS number as a string.
+
+    Returns:
+        value [int]: Checksum digit.
+    """
+    pis_digits = list(map(int, base_pis))
+    pis_sum = sum(digit * weight for digit, weight in zip(pis_digits, WEIGHTS))
+    check_digit = 11 - (pis_sum % 11)
+
+    return 0 if check_digit in [10, 11] else check_digit
diff --git a/poetry.lock b/poetry.lock
index 0e052a3..410e062 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
 
 [[package]]
 name = "black"
@@ -63,13 +63,13 @@ files = [
 
 [[package]]
 name = "click"
-version = "8.1.3"
+version = "8.1.7"
 description = "Composable command line interface toolkit"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
 ]
 
 [package.dependencies]
@@ -161,13 +161,13 @@ toml = ["tomli"]
 
 [[package]]
 name = "distlib"
-version = "0.3.6"
+version = "0.3.7"
 description = "Distribution utilities"
 optional = false
 python-versions = "*"
 files = [
-    {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
-    {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+    {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+    {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
 ]
 
 [[package]]
@@ -246,43 +246,43 @@ setuptools = "*"
 
 [[package]]
 name = "packaging"
-version = "23.1"
+version = "23.2"
 description = "Core utilities for Python packages"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
-    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
 ]
 
 [[package]]
 name = "pathspec"
-version = "0.11.1"
+version = "0.11.2"
 description = "Utility library for gitignore style pattern matching of file paths."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
-    {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+    {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
+    {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
 ]
 
 [[package]]
 name = "platformdirs"
-version = "3.6.0"
+version = "3.10.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "platformdirs-3.6.0-py3-none-any.whl", hash = "sha256:ffa199e3fbab8365778c4a10e1fbf1b9cd50707de826eb304b50e57ec0cc8d38"},
-    {file = "platformdirs-3.6.0.tar.gz", hash = "sha256:57e28820ca8094678b807ff529196506d7a21e17156cb1cddb3e74cebce54640"},
+    {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
+    {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
 ]
 
 [package.dependencies]
-typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""}
+typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""}
 
 [package.extras]
-docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
 
 [[package]]
 name = "pre-commit"
@@ -305,62 +305,72 @@ virtualenv = ">=20.10.0"
 
 [[package]]
 name = "pyyaml"
-version = "6.0"
+version = "6.0.1"
 description = "YAML parser and emitter for Python"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
-    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
-    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
-    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
-    {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
-    {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
-    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
-    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
-    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
-    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
-    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
-    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
-    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
-    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
-    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
-    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
-    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
-    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 ]
 
 [[package]]
 name = "setuptools"
-version = "67.8.0"
+version = "68.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"},
-    {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"},
+    {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
+    {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
 ]
 
 [package.extras]
@@ -381,68 +391,85 @@ files = [
 
 [[package]]
 name = "typed-ast"
-version = "1.5.4"
+version = "1.5.5"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
-    {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
-    {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
-    {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
-    {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
+    {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"},
+    {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"},
+    {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"},
+    {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"},
+    {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"},
+    {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"},
+    {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"},
+    {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"},
+    {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"},
+    {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"},
+    {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"},
+    {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"},
+    {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"},
+    {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"},
+    {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"},
+    {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"},
+    {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"},
+    {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"},
+    {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"},
+    {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"},
+    {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"},
+    {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"},
+    {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"},
+    {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"},
+    {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"},
+    {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"},
+    {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"},
+    {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"},
+    {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"},
+    {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"},
+    {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"},
 ]
 
 [[package]]
 name = "typing-extensions"
-version = "4.6.3"
+version = "4.7.1"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"},
-    {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"},
+    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
 ]
 
 [[package]]
 name = "virtualenv"
-version = "20.23.1"
+version = "20.24.5"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"},
-    {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"},
+    {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
+    {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
 ]
 
 [package.dependencies]
-distlib = ">=0.3.6,<1"
-filelock = ">=3.12,<4"
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
 importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
-platformdirs = ">=3.5.1,<4"
+platformdirs = ">=3.9.1,<4"
 
 [package.extras]
-docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
-test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
 
 [[package]]
 name = "zipp"
diff --git a/pyproject.toml b/pyproject.toml
index 22de0d7..7d4f750 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,12 @@
 [tool.poetry]
 name = "brutils"
-version = "1.0.3"
+version = "2.0.0"
 description = "Utils library for specific Brazilian businesses"
 authors = ["The Brazilian Utils Organization"]
 license = "MIT"
 readme = "README.md"
 repository = "https://github.com/brazilian-utils/brutils"
-keywords = ["cpf", "cnpj", "document", "validation", "brazil", "brazilian"]
+keywords = ["cpf", "cnpj", "cep", "document", "validation", "brazil", "brazilian"]
 classifiers = [
   "Development Status :: 5 - Production/Stable",
   "License :: OSI Approved :: MIT License",
@@ -28,7 +28,6 @@ classifiers = [
 [tool.poetry.dependencies]
 python = "^3.7"
 
-
 [tool.poetry.group.test.dependencies]
 coverage = "^7.2.7"
 

test_patch: diff --git a/tests/test_cep.py b/tests/test_cep.py
index 56b12a6..5e3544c 100644
--- a/tests/test_cep.py
+++ b/tests/test_cep.py
@@ -18,44 +18,44 @@ from brutils.cep import (
 from unittest import TestCase, main
 
 
-class CEP(TestCase):
+class TestCEP(TestCase):
     def test_remove_symbols(self):
-        assert remove_symbols("00000000") == "00000000"
-        assert remove_symbols("01310-200") == "01310200"
-        assert remove_symbols("01..310.-200.-") == "01310200"
-        assert remove_symbols("abc01310200*!*&#") == "abc01310200*!*&#"
-        assert (
-            remove_symbols("ab.c1.--.3-102.-0-.0-.*.-!*&#") == "abc1310200*!*&#"
+        self.assertEqual(remove_symbols("00000000"), "00000000")
+        self.assertEqual(remove_symbols("01310-200"), "01310200")
+        self.assertEqual(remove_symbols("01..310.-200.-"), "01310200")
+        self.assertEqual(remove_symbols("abc01310200*!*&#"), "abc01310200*!*&#")
+        self.assertEqual(
+            remove_symbols("ab.c1.--.3-102.-0-.0-.*.-!*&#"), "abc1310200*!*&#"
         )
-        assert remove_symbols("...---...") == ""
+        self.assertEqual(remove_symbols("...---..."), "")
 
     def test_format_cep(self):
         with patch("brutils.cep.is_valid", return_value=True) as mock_is_valid:
             # When cep is_valid, returns formatted cep
-            assert format_cep("01310200") == "01310-200"
+            self.assertEqual(format_cep("01310200"), "01310-200")
 
         # Checks if function is_valid_cnpj is called
         mock_is_valid.assert_called_once_with("01310200")
 
         with patch("brutils.cep.is_valid", return_value=False) as mock_is_valid:
             # When cep isn't valid, returns None
-            assert format_cep("013102009") is None
+            self.assertIsNone(format_cep("013102009"))
 
     def test_is_valid(self):
         # When CEP is not string, returns False
-        assert not is_valid(1)
+        self.assertIs(is_valid(1), False)
 
         # When CEP's len is different of 8, returns False
-        assert not is_valid("1")
+        self.assertIs(is_valid("1"), False)
 
         # When CEP does not contain only digits, returns False
-        assert not is_valid("1234567-")
+        self.assertIs(is_valid("1234567-"), False)
 
         # When CEP is valid
-        assert is_valid("99999999")
-        assert is_valid("88390000")
+        self.assertIs(is_valid("99999999"), True)
+        self.assertIs(is_valid("88390000"), True)
 
     def test_generate(self):
         for _ in range(10_000):
-            assert is_valid(generate())
+            self.assertIs(is_valid(generate()), True)
         # assert format(generate()) is not None
diff --git a/tests/test_cnpj.py b/tests/test_cnpj.py
index af5852c..09fea7c 100644
--- a/tests/test_cnpj.py
+++ b/tests/test_cnpj.py
@@ -23,14 +23,16 @@ from brutils.cnpj import (
 from unittest import TestCase, main
 
 
-class CNPJ(TestCase):
+class TestCNPJ(TestCase):
     def test_sieve(self):
-        assert sieve("00000000000") == "00000000000"
-        assert sieve("12.345.678/0001-90") == "12345678000190"
-        assert sieve("134..2435/.-1892.-") == "13424351892"
-        assert sieve("abc1230916*!*&#") == "abc1230916*!*&#"
-        assert sieve("ab.c1.--.2-3/09.-1-./6/-.*.-!*&#") == "abc1230916*!*&#"
-        assert sieve("/...---.../") == ""
+        self.assertEqual(sieve("00000000000"), "00000000000")
+        self.assertEqual(sieve("12.345.678/0001-90"), "12345678000190")
+        self.assertEqual(sieve("134..2435/.-1892.-"), "13424351892")
+        self.assertEqual(sieve("abc1230916*!*&#"), "abc1230916*!*&#")
+        self.assertEqual(
+            sieve("ab.c1.--.2-3/09.-1-./6/-.*.-!*&#"), "abc1230916*!*&#"
+        )
+        self.assertEqual(sieve("/...---.../"), "")
 
     def test_remove_symbols(self):
         with patch("brutils.cnpj.sieve") as mock_sieve:
@@ -39,15 +41,17 @@ class CNPJ(TestCase):
             mock_sieve.assert_called()
 
     def test_display(self):
-        assert display("00000000000109") == "00.000.000/0001-09"
-        assert display("00000000000000") is None
-        assert display("0000000000000a") is None
-        assert display("0000000000000") is None
+        self.assertEqual(display("00000000000109"), "00.000.000/0001-09")
+        self.assertIsNone(display("00000000000000"))
+        self.assertIsNone(display("0000000000000"))
+        self.assertIsNone(display("0000000000000a"))
 
     def test_format_cnpj(self):
         with patch("brutils.cnpj.is_valid", return_value=True) as mock_is_valid:
             # When cnpj is_valid, returns formatted cnpj
-            assert format_cnpj("01838723000127") == "01.838.723/0001-27"
+            self.assertEqual(
+                format_cnpj("01838723000127"), "01.838.723/0001-27"
+            )
 
         # Checks if function is_valid_cnpj is called
         mock_is_valid.assert_called_once_with("01838723000127")
@@ -56,56 +60,56 @@ class CNPJ(TestCase):
             "brutils.cnpj.is_valid", return_value=False
         ) as mock_is_valid:
             # When cnpj isn't valid, returns None
-            assert format_cnpj("01838723000127") is None
+            self.assertIsNone(format_cnpj("01838723000127"))
 
     def test_validate(self):
-        assert validate("34665388000161")
-        assert not validate("52599927000100")
-        assert not validate("00000000000")
+        self.assertIs(validate("34665388000161"), True)
+        self.assertIs(validate("52599927000100"), False)
+        self.assertIs(validate("00000000000"), False)
 
     def test_is_valid(self):
         # When CNPJ is not string, returns False
-        assert not is_valid(1)
+        self.assertIs(is_valid(1), False)
 
         # When CNPJ's len is different of 14, returns False
-        assert not is_valid("1")
+        self.assertIs(is_valid("1"), False)
 
         # When CNPJ does not contain only digits, returns False
-        assert not is_valid("1112223334445-")
+        self.assertIs(is_valid("1112223334445-"), False)
 
         # When CNPJ has only the same digit, returns false
-        assert not is_valid("11111111111111")
+        self.assertIs(is_valid("11111111111111"), False)
 
         # When rest_1 is lt 2 and the 13th digit is not 0, returns False
-        assert not is_valid("1111111111315")
+        self.assertIs(is_valid("1111111111315"), False)
 
         # When rest_1 is gte 2 and the 13th digit is not (11 - rest), returns False
-        assert not is_valid("1111111111115")
+        self.assertIs(is_valid("1111111111115"), False)
 
         # When rest_2 is lt 2 and the 14th digit is not 0, returns False
-        assert not is_valid("11111111121205")
+        self.assertIs(is_valid("11111111121205"), False)
 
         # When rest_2 is gte 2 and the 14th digit is not (11 - rest), returns False
-        assert not is_valid("11111111113105")
+        self.assertIs(is_valid("11111111113105"), False)
 
         # When CNPJ is valid
-        assert is_valid("34665388000161")
-        assert is_valid("01838723000127")
+        self.assertIs(is_valid("34665388000161"), True)
+        self.assertIs(is_valid("01838723000127"), True)
 
     def test_generate(self):
         for _ in range(10_000):
-            assert validate(generate())
-            assert display(generate()) is not None
+            self.assertIs(validate(generate()), True)
+            self.assertIsNotNone(display(generate()))
 
     def test__hashdigit(self):
-        assert _hashdigit("00000000000000", 13) == 0
-        assert _hashdigit("00000000000000", 14) == 0
-        assert _hashdigit("52513127000292", 13) == 9
-        assert _hashdigit("52513127000292", 14) == 9
+        self.assertEqual(_hashdigit("00000000000000", 13), 0)
+        self.assertEqual(_hashdigit("00000000000000", 14), 0)
+        self.assertEqual(_hashdigit("52513127000292", 13), 9)
+        self.assertEqual(_hashdigit("52513127000292", 14), 9)
 
     def test__checksum(self):
-        assert _checksum("00000000000000") == "00"
-        assert _checksum("52513127000299") == "99"
+        self.assertEqual(_checksum("00000000000000"), "00")
+        self.assertEqual(_checksum("52513127000299"), "99")
 
 
 if __name__ == "__main__":
diff --git a/tests/test_cpf.py b/tests/test_cpf.py
index ff6aa4b..37b2506 100644
--- a/tests/test_cpf.py
+++ b/tests/test_cpf.py
@@ -23,14 +23,16 @@ from brutils.cpf import (
 from unittest import TestCase, main
 
 
-class CPF(TestCase):
+class TestCPF(TestCase):
     def test_sieve(self):
-        assert sieve("00000000000") == "00000000000"
-        assert sieve("123.456.789-10") == "12345678910"
-        assert sieve("134..2435.-1892.-") == "13424351892"
-        assert sieve("abc1230916*!*&#") == "abc1230916*!*&#"
-        assert sieve("ab.c1.--.2-309.-1-.6-.*.-!*&#") == "abc1230916*!*&#"
-        assert sieve("...---...") == ""
+        self.assertEqual(sieve("00000000000"), "00000000000")
+        self.assertEqual(sieve("123.456.789-10"), "12345678910")
+        self.assertEqual(sieve("134..2435.-1892.-"), "13424351892")
+        self.assertEqual(sieve("abc1230916*!*&#"), "abc1230916*!*&#")
+        self.assertEqual(
+            sieve("ab.c1.--.2-309.-1-.6-.*.-!*&#"), "abc1230916*!*&#"
+        )
+        self.assertEqual(sieve("...---..."), "")
 
     def test_remove_symbols(self):
         with patch("brutils.cpf.sieve") as mock_sieve:
@@ -39,71 +41,71 @@ class CPF(TestCase):
             mock_sieve.assert_called()
 
     def test_display(self):
-        assert display("00000000011") == "000.000.000-11"
-        assert display("00000000000") is None
-        assert display("0000000000a") is None
-        assert display("000000000000") is None
+        self.assertEqual(display("00000000011"), "000.000.000-11")
+        self.assertIsNone(display("00000000000"))
+        self.assertIsNone(display("0000000000a"))
+        self.assertIsNone(display("000000000000"))
 
     def test_format_cpf(self):
         with patch("brutils.cpf.is_valid", return_value=True) as mock_is_valid:
             # When cpf is_valid, returns formatted cpf
-            assert format_cpf("11144477735") == "111.444.777-35"
+            self.assertEqual(format_cpf("11144477735"), "111.444.777-35")
 
         # Checks if function is_valid_cpf is called
         mock_is_valid.assert_called_once_with("11144477735")
 
         with patch("brutils.cpf.is_valid", return_value=False) as mock_is_valid:
             # When cpf isn't valid, returns None
-            assert format_cpf("11144477735") is None
+            self.assertIsNone(format_cpf("11144477735"))
 
     def test_validate(self):
-        assert validate("52513127765")
-        assert validate("52599927765")
-        assert not validate("00000000000")
+        self.assertIs(validate("52513127765"), True)
+        self.assertIs(validate("52599927765"), True)
+        self.assertIs(validate("00000000000"), False)
 
     def test_is_valid(self):
         # When cpf is not string, returns False
-        assert not is_valid(1)
+        self.assertIs(is_valid(1), False)
 
         # When cpf's len is different of 11, returns False
-        assert not is_valid("1")
+        self.assertIs(is_valid("1"), False)
 
         # When cpf does not contain only digits, returns False
-        assert not is_valid("1112223334-")
+        self.assertIs(is_valid("1112223334-"), False)
 
         # When CPF has only the same digit, returns false
-        assert not is_valid("11111111111")
+        self.assertIs(is_valid("11111111111"), False)
 
         # When rest_1 is lt 2 and the 10th digit is not 0, returns False
-        assert not is_valid("11111111215")
+        self.assertIs(is_valid("11111111215"), False)
 
         # When rest_1 is gte 2 and the 10th digit is not (11 - rest), returns False
-        assert not is_valid("11144477705")
+        self.assertIs(is_valid("11144477705"), False)
 
         # When rest_2 is lt 2 and the 11th digit is not 0, returns False
-        assert not is_valid("11111111204")
+        self.assertIs(is_valid("11111111204"), False)
 
         # When rest_2 is gte 2 and the 11th digit is not (11 - rest), returns False
-        assert not is_valid("11144477732")
+        self.assertIs(is_valid("11144477732"), False)
 
         # When cpf is valid
-        assert is_valid("11144477735")
-        assert is_valid("11111111200")
+        self.assertIs(is_valid("11144477735"), True)
+        self.assertIs(is_valid("11111111200"), True)
 
     def test_generate(self):
         for _ in range(10_000):
-            assert validate(generate())
-            assert display(generate()) is not None
+            self.assertIs(validate(generate()), True)
+            self.assertIsNotNone(display(generate()))
 
     def test__hashdigit(self):
-        assert _hashdigit("000000000", 10) == 0
-        assert _hashdigit("0000000000", 11) == 0
-        assert _hashdigit("52513127765", 10) == 6
-        assert _hashdigit("52513127765", 11) == 5
+        self.assertEqual(_hashdigit("000000000", 10), 0)
+        self.assertEqual(_hashdigit("0000000000", 11), 0)
+        self.assertEqual(_hashdigit("52513127765", 10), 6)
+        self.assertEqual(_hashdigit("52513127765", 11), 5)
 
     def test_checksum(self):
-        assert _checksum("000000000") == "00"
-        assert _checksum("525131277") == "65"
+        self.assertEqual(_checksum("000000000"), "00")
+        self.assertEqual(_checksum("525131277"), "65")
 
 
 if __name__ == "__main__":
diff --git a/tests/test_email.py b/tests/test_email.py
new file mode 100644
index 0000000..b49b2b8
--- /dev/null
+++ b/tests/test_email.py
@@ -0,0 +1,53 @@
+import unittest
+from brutils import is_valid_email
+
+
+class TestEmailValidation(unittest.TestCase):
+    def test_valid_email(self):
+        # Valid email addresses
+        valid_emails = [
+            "joao.ninguem@gmail.com",
+            "user123@gmail.com",
+            "test.email@mydomain.co.uk",
+            "johndoe@sub.domain.example",
+            "f99999999@place.university-campus.ac.in",
+        ]
+        for email in valid_emails:
+            try:
+                self.assertTrue(is_valid_email(email))
+            except:
+                print(f"AssertionError for email: {email}")
+                raise AssertionError
+
+    def test_invalid_email(self):
+        # Invalid email addresses
+        invalid_emails = [
+            ".joao.ninguem@gmail.com",
+            "joao ninguem@gmail.com",
+            "not_an_email",
+            "@missing_username.com",
+            "user@incomplete.",
+            "user@.incomplete",
+            "user@inva!id.com",
+            "user@missing-tld.",
+        ]
+        for email in invalid_emails:
+            try:
+                self.assertFalse(is_valid_email(email))
+            except:
+                print(f"AssertionError for email: {email}")
+                raise AssertionError
+
+    def test_non_string_input(self):
+        # Non-string input should return False
+        non_strings = [None, 123, True, ["test@example.com"]]
+        for value in non_strings:
+            self.assertFalse(is_valid_email(value))
+
+    def test_empty_string(self):
+        # Empty string should return False
+        self.assertFalse(is_valid_email(""))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_license_plate.py b/tests/test_license_plate.py
new file mode 100644
index 0000000..4f0f832
--- /dev/null
+++ b/tests/test_license_plate.py
@@ -0,0 +1,42 @@
+# from unittest.mock import patch
+
+# from brutils.phone import is_valid_landline, is_valid_mobile, is_valid
+from brutils.license_plate import is_valid_mercosul
+
+from unittest import TestCase, main
+
+
+class TestLicensePlate(TestCase):
+    def test_is_valid_license_plate_mercosul(self):
+        # When license plate is not string, returns False
+        self.assertIs(is_valid_mercosul(1234567), False)
+
+        # When license plate doesn't match the pattern LLLNLNN, returns False
+        self.assertIs(is_valid_mercosul("ABCDEFG"), False)
+        self.assertIs(is_valid_mercosul("1234567"), False)
+        self.assertIs(is_valid_mercosul("ABC4567"), False)
+        self.assertIs(is_valid_mercosul("ABCD567"), False)
+        self.assertIs(is_valid_mercosul("ABC45F7"), False)
+        self.assertIs(is_valid_mercosul("ABC456G"), False)
+        self.assertIs(is_valid_mercosul("ABC123"), False)
+
+        # When license plate is an empty string, returns False
+        self.assertIs(is_valid_mercosul(""), False)
+
+        # When license plate's length is different of 7, returns False
+        self.assertIs(is_valid_mercosul("ABC4E678"), False)
+
+        # When license plate has separator, returns false
+        self.assertIs(is_valid_mercosul("ABC-1D23"), False)
+
+        # When license plate is valid
+        self.assertIs(is_valid_mercosul("ABC4E67"), True)
+        self.assertIs(is_valid_mercosul("AAA1A11"), True)
+        self.assertIs(is_valid_mercosul("XXX9X99"), True)
+
+        # Check if function is case insensitive
+        self.assertIs(is_valid_mercosul("abc4e67"), True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/test_license_plate_old_format.py b/tests/test_license_plate_old_format.py
new file mode 100644
index 0000000..66efcd3
--- /dev/null
+++ b/tests/test_license_plate_old_format.py
@@ -0,0 +1,29 @@
+from unittest import TestCase, main
+from brutils.license_plate import is_valid_license_plate_old_format
+
+
+class TestLicenSePlate(TestCase):
+    def test_is_valid_license_old_format(self):
+        # When license plate is valid, returns True
+        self.assertTrue(is_valid_license_plate_old_format("ABC1234"))
+        self.assertTrue(is_valid_license_plate_old_format("abc1234"))
+
+        # When license plate is valid with whitespaces, returns True
+        self.assertTrue(is_valid_license_plate_old_format(" ABC1234    "))
+
+        # When license plate is not string, returns False
+        self.assertFalse(is_valid_license_plate_old_format(123456))
+
+        # When license plate is invalid with special characters, returns False
+        self.assertFalse(is_valid_license_plate_old_format("ABC-1234"))
+
+        # When license plate is invalid with numbers and letters out of order, returns False
+        self.assertFalse(is_valid_license_plate_old_format("A1CA23W"))
+
+        # When license plate is invalid with new format, returns False
+        self.assertFalse(is_valid_license_plate_old_format("ABC1D23"))
+        self.assertFalse(is_valid_license_plate_old_format("abcd123"))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/test_phone.py b/tests/test_phone.py
new file mode 100644
index 0000000..a32651a
--- /dev/null
+++ b/tests/test_phone.py
@@ -0,0 +1,149 @@
+from unittest.mock import patch
+
+from brutils.phone import (
+    is_valid_landline,
+    is_valid_mobile,
+    is_valid,
+    remove_symbols_phone,
+)
+
+from unittest import TestCase, main
+
+
+class TestPhone(TestCase):
+    def test_is_valid_landline(self):
+        # When landline phone is not string, returns False
+        self.assertIs(is_valid_landline(1938814933), False)
+
+        # When landline phone doesn't contain only digits, returns False
+        self.assertIs(is_valid_landline("(19)388149"), False)
+
+        # When landline phone is an empty string, returns False
+        self.assertIs(is_valid_landline(""), False)
+
+        # When landline phone's len is different of 10, returns False
+        self.assertIs(is_valid_landline("193881"), False)
+
+        # When landline phone's first digit is 0, returns False
+        self.assertIs(is_valid_landline("0938814933"), False)
+
+        # When landline phone's second digit is 0, returns False
+        self.assertIs(is_valid_landline("1038814933"), False)
+
+        # When landline phone's third digit is different of 2,3,4 or 5, returns False
+        self.assertIs(is_valid_landline("1998814933"), False)
+
+        # When landline phone is valid
+        self.assertIs(is_valid_landline("1928814933"), True)
+        self.assertIs(is_valid_landline("1938814933"), True)
+        self.assertIs(is_valid_landline("1948814933"), True)
+        self.assertIs(is_valid_landline("1958814933"), True)
+        self.assertIs(is_valid_landline("3333333333"), True)
+
+    def test_is_valid_mobile(self):
+        # When mobile is not string, returns False
+        self.assertIs(is_valid_mobile(1), False)
+
+        # When mobile doesn't contain only digits, returns False
+        self.assertIs(is_valid_mobile(119940 - 2927), False)
+
+        # When mobile is an empty string, returns False
+        self.assertIs(is_valid_mobile(""), False)
+
+        # When mobile's len is different of 11, returns False
+        self.assertIs(is_valid_mobile("119940"), False)
+
+        # When mobile's first digit is 0, returns False
+        self.assertIs(is_valid_mobile("01994029275"), False)
+
+        # When mobile's second digit is 0, returns False
+        self.assertIs(is_valid_mobile("90994029275"), False)
+
+        # When mobile's third digit is different of 9, returns False
+        self.assertIs(is_valid_mobile("11594029275"), False)
+
+        # When mobile is valid
+        self.assertIs(is_valid_mobile("99999999999"), True)
+        self.assertIs(is_valid_mobile("11994029275"), True)
+
+    def test_is_valid(self):
+        # Mock is_valid_landline to return True
+        with patch(
+            "brutils.phone.is_valid_landline", return_value=True
+        ) as mock_is_valid_landline:
+            with patch("brutils.phone.is_valid_mobile") as mock_is_valid_mobile:
+                # When landline phone is_valid, returns True
+                self.assertIs(is_valid("1958814933"), True)
+
+                # Checks if function is_valid_landline is called with the correct argument
+                mock_is_valid_landline.assert_called_once_with("1958814933")
+                # Checks if function is_valid_mobile is not called
+                mock_is_valid_mobile.assert_not_called()
+
+        # Mock is_valid_landline to return False, is_valid_mobile return True
+        with patch(
+            "brutils.phone.is_valid_landline", return_value=False
+        ) as mock_is_valid_landline:
+            with patch(
+                "brutils.phone.is_valid_mobile", return_value=True
+            ) as mock_is_valid_mobile:
+                # When mobile phone is_valid, returns True
+                self.assertIs(is_valid("11994029275"), True)
+
+                # Checks if function is_valid_landline is called with the correct argument
+                mock_is_valid_landline.assert_called_once_with("11994029275")
+                # Checks if function is_valid_mobile is called with the correct argument
+                mock_is_valid_mobile.assert_called_once_with("11994029275")
+
+        # Mock is_valid_landline to return False, is_valid_mobile return False
+        with patch(
+            "brutils.phone.is_valid_landline", return_value=False
+        ) as mock_is_valid_landline:
+            with patch(
+                "brutils.phone.is_valid_mobile", return_value=False
+            ) as mock_is_valid_mobile:
+                # When landline phone isn't valid, returns False
+                self.assertIs(is_valid("11994029275"), False)
+
+                # Checks if function is_valid_landline is called with the correct argument
+                mock_is_valid_landline.assert_called_once_with("11994029275")
+                # Checks if function is_valid_mobile is called with the correct argument
+                mock_is_valid_mobile.assert_called_once_with("11994029275")
+
+    def test_remove_symbols_phone(self):
+        # When the string empty, it returns an empty string
+        self.assertEqual(remove_symbols_phone(""), "")
+
+        # When there are no symbols to remove, it returns the same string
+        self.assertEqual(remove_symbols_phone("21994029275"), "21994029275")
+
+        # When there are symbols to remove, it returns the string without symbols
+        self.assertEqual(remove_symbols_phone("(21)99402-9275"), "21994029275")
+        self.assertEqual(remove_symbols_phone("(21)2569-6969"), "2125696969")
+
+        # When there are extra symbols, it only removes the specified symbols
+        self.assertEqual(
+            remove_symbols_phone("(21) 99402-9275!"), "21994029275!"
+        )
+
+        # When the string contains non-numeric characters, it returns the string without the specified symbols
+        self.assertEqual(remove_symbols_phone("(21)ABC-DEF"), "21ABCDEF")
+
+        # When the phone number contains a plus symbol and spaces, they are removed
+        self.assertEqual(
+            remove_symbols_phone("+55 21 99402-9275"), "5521994029275"
+        )
+
+        # When the phone number contains multiple spaces, all are removed
+        self.assertEqual(
+            remove_symbols_phone("55 21  99402 9275"), "5521994029275"
+        )
+
+        # When the phone number contains a mixture of all specified symbols, all are removed
+        self.assertEqual(
+            remove_symbols_phone("+55 (21) 99402-9275"), "5521994029275"
+        )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/test_pis.py b/tests/test_pis.py
new file mode 100644
index 0000000..bd81287
--- /dev/null
+++ b/tests/test_pis.py
@@ -0,0 +1,58 @@
+from os import pardir
+from os.path import abspath, join, dirname
+from sys import path, version_info, dont_write_bytecode
+from inspect import getsourcefile
+from unittest.mock import patch
+
+dont_write_bytecode = True
+range = range if version_info.major >= 3 else xrange
+path.insert(
+    1, abspath(join(dirname(abspath(getsourcefile(lambda: 0))), pardir))
+)
+from brutils.pis import validate, is_valid, _checksum
+from unittest import TestCase, main
+
+
+class TestPIS(TestCase):
+    def test_validate(self):
+        self.assertIs(validate("123456789ab"), False)
+        self.assertIs(validate("901.85930.32-3"), False)
+        self.assertIs(validate("90185930323"), True)
+        self.assertIs(validate("93616217820"), True)
+
+    def test_is_valid(self):
+        # When PIS is not string, returns False
+        self.assertIs(is_valid(1), False)
+        self.assertIs(is_valid([]), False)
+        self.assertIs(is_valid({}), False)
+        self.assertIs(is_valid(None), False)
+
+        # When PIS's len is different of 11, returns False
+        self.assertIs(is_valid("123456789"), False)
+
+        # When PIS does not contain only digits, returns False
+        self.assertIs(is_valid("123pis"), False)
+
+        # When checksum digit doesn't match last digit, returns False
+        self.assertIs(is_valid("11111111111"), False)
+        self.assertIs(is_valid("11111111215"), False)
+        self.assertIs(is_valid("12038619493"), False)
+
+        # When PIS is valid
+        self.assertIs(is_valid("12038619494"), True)
+        self.assertIs(is_valid("12016784018"), True)
+        self.assertIs(is_valid("12083210826"), True)
+
+    def test_checksum(self):
+        # Checksum digit is 0 when the subtracted number is 10 or 11
+        self.assertEqual(_checksum("1204152015"), 0)
+        self.assertEqual(_checksum("1204433157"), 0)
+
+        # Checksum digit is equal the subtracted number
+        self.assertEqual(_checksum("1204917738"), 2)
+        self.assertEqual(_checksum("1203861949"), 4)
+        self.assertEqual(_checksum("1208321082"), 6)
+
+
+if __name__ == "__main__":
+    main()


----

problem_statement: Gerar nÃºmero de PIS vÃ¡lido e aleatÃ³rio
**Seu pedido de recurso estÃ¡ relacionado a um problema? Por favor, descreva.**
Gerar um nÃºmero de PIS vÃ¡lido e aleatÃ³rio.

**Descreva a soluÃ§Ã£o que vocÃª gostaria**
- Chamar um novo utilitÃ¡rio que retorna um nÃºmero de PIS vÃ¡lido e aleatÃ³rio.
- Sem formataÃ§Ã£o

**Descreva alternativas que vocÃª considerou**
- Criar um novo utilitÃ¡rio generate_pis:
```python
def generate_pis():
...
```
Exemplo de uso:
```
from brutils import generate_pis

>>> generate_pis()
"12042644708"
>>> generate_pis()
"12021423567"
```

**Contexto adicional**
- https://pt.wikipedia.org/wiki/PIS/PASEP
- https://www.macoratti.net/alg_pis.htm

date: 2023-10-05
patch: diff --git a/brutils/__init__.py b/brutils/__init__.py
index a50b1a2..8bc0b0d 100644
--- a/brutils/__init__.py
+++ b/brutils/__init__.py
@@ -27,4 +27,9 @@ from brutils.license_plate import (
 )
 
 from brutils.email import is_valid as is_valid_email
+
 from brutils.license_plate import is_valid_license_plate_old_format
+
+from brutils.pis import (
+    is_valid as is_valid_pis,
+)
diff --git a/brutils/pis.py b/brutils/pis.py
new file mode 100644
index 0000000..ddb213b
--- /dev/null
+++ b/brutils/pis.py
@@ -0,0 +1,57 @@
+WEIGHTS = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
+
+
+# OPERATIONS
+############
+
+
+def validate(pis: str) -> bool:
+    """
+    Validate a Brazilian PIS number.
+
+    The PIS is valid if:
+    - It has 11 digits
+    - All characters are digits
+    - It passes the weight calculation check
+
+    Args:
+        pis[str]: PIS number as a string.
+
+    Returns:
+        value[bool]: True if PIS is valid, False otherwise.
+    """
+    if len(pis) != 11 or not pis.isdigit():
+        return False
+
+    return pis[-1] == str(_checksum(pis[:-1]))
+
+
+def is_valid(pis: str) -> bool:
+    """
+    Returns whether or not the verifying checksum digit of the
+    given `pis` match its base number.
+
+    Args:
+        pis[str]: PIS number as a string of proper length.
+
+    Returns:
+        value[bool]: True if PIS is valid, False otherwise.
+    """
+    return isinstance(pis, str) and validate(pis)
+
+
+def _checksum(base_pis: str) -> int:
+    """
+    Calculates the checksum digit of the given `base_pis` string.
+
+    Args:
+        base_pis [str]: 10 first digits of PIS number as a string.
+
+    Returns:
+        value [int]: Checksum digit.
+    """
+    pis_digits = list(map(int, base_pis))
+    pis_sum = sum(digit * weight for digit, weight in zip(pis_digits, WEIGHTS))
+    check_digit = 11 - (pis_sum % 11)
+
+    return 0 if check_digit in [10, 11] else check_digit

test_patch: commit 7947d33471b283dce3189c842caa301bcbfab30d
Author: Kaio Duarte <duartedossantos@gmail.com>
Date:   Thu Oct 5 20:04:11 2023 +0100

    feat: add is_valid_pis (#216)
    
    * feat: add is_valid_pis
    
    * chore: update README

diff --git a/tests/test_pis.py b/tests/test_pis.py
new file mode 100644
index 0000000..bd81287
--- /dev/null
+++ b/tests/test_pis.py
@@ -0,0 +1,58 @@
+from os import pardir
+from os.path import abspath, join, dirname
+from sys import path, version_info, dont_write_bytecode
+from inspect import getsourcefile
+from unittest.mock import patch
+
+dont_write_bytecode = True
+range = range if version_info.major >= 3 else xrange
+path.insert(
+    1, abspath(join(dirname(abspath(getsourcefile(lambda: 0))), pardir))
+)
+from brutils.pis import validate, is_valid, _checksum
+from unittest import TestCase, main
+
+
+class TestPIS(TestCase):
+    def test_validate(self):
+        self.assertIs(validate("123456789ab"), False)
+        self.assertIs(validate("901.85930.32-3"), False)
+        self.assertIs(validate("90185930323"), True)
+        self.assertIs(validate("93616217820"), True)
+
+    def test_is_valid(self):
+        # When PIS is not string, returns False
+        self.assertIs(is_valid(1), False)
+        self.assertIs(is_valid([]), False)
+        self.assertIs(is_valid({}), False)
+        self.assertIs(is_valid(None), False)
+
+        # When PIS's len is different of 11, returns False
+        self.assertIs(is_valid("123456789"), False)
+
+        # When PIS does not contain only digits, returns False
+        self.assertIs(is_valid("123pis"), False)
+
+        # When checksum digit doesn't match last digit, returns False
+        self.assertIs(is_valid("11111111111"), False)
+        self.assertIs(is_valid("11111111215"), False)
+        self.assertIs(is_valid("12038619493"), False)
+
+        # When PIS is valid
+        self.assertIs(is_valid("12038619494"), True)
+        self.assertIs(is_valid("12016784018"), True)
+        self.assertIs(is_valid("12083210826"), True)
+
+    def test_checksum(self):
+        # Checksum digit is 0 when the subtracted number is 10 or 11
+        self.assertEqual(_checksum("1204152015"), 0)
+        self.assertEqual(_checksum("1204433157"), 0)
+
+        # Checksum digit is equal the subtracted number
+        self.assertEqual(_checksum("1204917738"), 2)
+        self.assertEqual(_checksum("1203861949"), 4)
+        self.assertEqual(_checksum("1208321082"), 6)
+
+
+if __name__ == "__main__":
+    main()