Repository: brazilian-utils/brutils-python (python)

Issue:
Adicionar método is_holiday para verificar feriados nacionais e estaduais
**Seu pedido de recurso está relacionado a um problema? Por favor, descreva.**

Quero verificar se uma data específica é um feriado nacional ou estadual no Brasil. O método deve aceitar uma data no formato `datetime` e uma unidade federativa (UF) opcional. Caso a data ou a UF seja inválida, o método deve retornar `None`.

Por exemplo, verificar se `datetime(2024, 1, 1)` é um feriado nacional e retornar `True`. Caso a data ou a UF seja inválida, o método deve retornar `None`.

**Descreva a solução que você gostaria**

* Uma função `is_holiday`, que recebe um objeto `datetime` obrigatório e uma string opcional `uf`, e retorna um booleano indicando se a data é um feriado, ou `None` se a data ou a UF forem inválidas.
* A função deve utilizar a biblioteca [holidays](https://pypi.org/project/holidays/) para realizar a verificação.
* Deve considerar feriados nacionais e, caso seja passada uma `uf` válida, verificar também os feriados estaduais.
* Se a data não for válida ou a UF não corresponder a um estado brasileiro, a função deve retornar `None`.
* O método não lida com feriados municipais

**Exemplo de Uso**:
```python
from datetime import datetime
from brutils.date import is_holiday

# Feriado nacional
data_feriado = datetime(2024, 1, 1)  # Ano Novo
resultado_feriado = is_holiday(data_feriado)
print(resultado_feriado)  # Deveria retornar True

# Dia normal
data_normal = datetime(2024, 1, 2)
resultado_normal = is_holiday(data_normal)
print(resultado_normal)  # Deveria retornar False

# Feriado estadual
data_feriado_estadual = datetime(2024, 7, 9)  # Revolução Constitucionalista (SP)
resultado_feriado_estadual = is_holiday(data_feriado_estadual, uf="SP")
print(resultado_feriado_estadual)  # Deveria retornar True

# UF inválida
data_uf_invalida = datetime(2024, 1, 1)
resultado_uf_invalida = is_holiday(data_uf_invalida, uf="XX")
print(resultado_uf_invalida)  # Deveria retornar None

# Data inválida
data_invalida = "01/01/2024"  # Formato incorreto
resultado_data_invalida = is_holiday(data_invalida)
print(resultado_data_invalida)  # Deveria retornar None
```

**Descreva alternativas que você considerou**

1. Criar o arquivo: `brutils-python/brutils/date.py`.

```python
from datetime import datetime

def is_holiday(date: datetime, uf: str = None) -> bool | None:
    """
    Checks if the given date is a national or state holiday in Brazil.

    This function takes a date as a `datetime` object and an optional UF (Unidade Federativa), 
    returning a boolean value indicating whether the date is a holiday or `None` if the date or 
    UF are invalid.

    The method does not handle municipal holidays.

    Args:
        date (datetime): The date to be checked.
        uf (str, optional): The state abbreviation (UF) to check for state holidays. 
                            If not provided, only national holidays will be considered.

    Returns:
        bool | None: Returns `True` if the date is a holiday, `False` if it is not, 
                     or `None` if the date or UF are invalid.

    Note:
        The function logic should be implemented using the `holidays` library.
        For more information, refer to the documentation at: https://pypi.org/project/holidays/

    Usage Examples:
        >>> from datetime import datetime
        >>> is_holiday(datetime(2024, 1, 1))
        True

        >>> is_holiday(datetime(2024, 1, 2))
        False

        >>> is_holiday(datetime(2024, 3, 2), uf="SP")
        False

        >>> is_holiday(datetime(2024, 12, 25), uf="RJ")
        True
    """
    # Adicionar lógica aqui
    pass
```

2. Importar a nova função no arquivo `brutils-python/brutils/__init__.py`.

    ```python
    # Date Imports    
    from brutils.date import is_holiday
    ```

3. Adicionar o nome da nova função na lista `__all__` do mesmo arquivo `brutils-python/brutils/__init__.py`:

    ```python
    __all__ = [
        ...
        # Date
        'is_holiday'
    ]
    ```

4. Criar o arquivo de teste: `brutils-python/tests/test_date.py`.

    ```python
    from unittest import TestCase
    from datetime import datetime
    from brutils.date import is_holiday

    class TestDate(TestCase):
        def test_is_holiday(self):
            # Testes com feriados válidos
            self.assertTrue(is_holiday(datetime(2024, 1, 1)))  # Ano Novo
            self.assertTrue(is_holiday(datetime(2024, 7, 9), uf="SP"))  # Revolução Constitucionalista (SP)

            # Testes com dias normais
            self.assertFalse(is_holiday(datetime(2024, 1, 2)))  # Dia normal
            self.assertFalse(is_holiday(datetime(2024, 7, 9), uf="RJ"))  # Dia normal no RJ

            # Testes com data inválida
            self.assertIsNone(is_holiday("2024-01-01"))  # Formato incorreto
            self.assertIsNone(is_holiday(None))  # Data None

            # Testes com UF inválida
            self.assertIsNone(is_holiday(datetime(2024, 1, 1), uf="XX"))  # UF inválida

            # Testes com limite de datas
            self.assertTrue(is_holiday(datetime(2024, 12, 25)))  # Natal
            self.assertTrue(is_holiday(datetime(2024, 11, 15)))  # Proclamação da República

            # Testar outros feriados e casos de borda
    ```

**Edge Cases**:
1. **Data no formato inválido**: Deve retornar `None`.
2. **UF inválida ou não existente**: Deve retornar `None`.
3. **Data sem UF fornecida**: Deve considerar apenas feriados nacionais.
4. **Data nos limites de feriados (véspera de feriados, como 24/12 e 31/12)**: Testar comportamento.
5. **Feriado em ano bissexto**: Verificar comportamento em anos como 2024.
6. **Primeiro feriado do ano e último feriado do ano**: Testar como o código lida com extremos.
7. **Datas passadas ou futuras além do calendário anual atual**: Testar comportamento com datas fora do ano atual.

5. Seguir os passos seguintes do [guia de contribuição][guia-de-contribuição].

**Contexto adicional**

* A verificação deve considerar tanto feriados nacionais quanto estaduais quando fornecida a `uf`.
* Edge cases como datas inválidas e UFs incorretas devem retornar `None`.

[holidays]: https://pypi.org/project/holidays/  
[guia-de-contribuição]: https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#primeira-contribui%C3%A7%C3%A3o



Pull request description:
Adicionar método is_holiday para verificar feriados nacionais e estaduais
## Descrição
Adicionar método is_holiday para verificar feriados nacionais e estaduais

## Mudanças Propostas
- Implementação do método is_holiday, que permite verificar se uma data é feriado nacional ou estadual usando a biblioteca holidays.
- Validação de parâmetros: verifica se o objeto de data é do tipo datetime e se a UF (Unidade Federativa) é válida.
- Compatibilidade com feriados nacionais e estaduais:
      - Verifica feriados nacionais quando apenas a data é fornecida.
      - Verifica feriados estaduais específicos caso uma UF válida seja informada.
- Retorno de valores:
      - Retorna True se a data for um feriado (nacional ou estadual).
      - Retorna False se a data não for um feriado.
      - Retorna None para entradas inválidas, como data ou UF inválidas.

## Checklist de Revisão
<!--- Marque as caixas que se aplicam. Você pode deixar caixas desmarcadas se elas não se aplicarem.-->

- [x] Eu li o [Contributing.md](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md)
- [x] Os testes foram adicionados ou atualizados para refletir as mudanças (se aplicável).
- [x] Foi adicionada uma entrada no changelog / Meu PR não necessita de uma nova entrada no changelog.
- [x] A [documentação](https://github.com/brazilian-utils/brutils-python/blob/main/README.md) em português foi atualizada ou criada, se necessário.
- [x] Se feita a documentação, a atualização do [arquivo em inglês](https://github.com/brazilian-utils/brutils-python/blob/main/README_EN.md). <!---Permitido uso de Google Tradutor/ChatGPT. -->
- [x] Eu documentei as minhas mudanças no código, adicionando docstrings e comentários. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#8-fa%C3%A7a-as-suas-altera%C3%A7%C3%B5es)
- [x] O código segue as diretrizes de estilo e padrões de codificação do projeto.
- [x] Todos os testes passam. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#testes)
- [x] O Pull Request foi testado localmente. [Instruções](https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#7-execute-o-brutils-localmente)
- [x] Não há conflitos de mesclagem.

## Comentários Adicionais (opcional)
<!--- Adicione qualquer informação adicional ou contexto que você acha importante para os revisores entenderem suas mudanças.-->

## Issue Relacionada
<!---Todos os PRs devem ter uma issue relacionada. Dessa forma, podemos garantir que ninguém perca tempo trabalhando em algo que não precisa ser feito. -->

Closes #423


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3211744..da7eb34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Utilitário `convert_code_to_uf` [#397](https://github.com/brazilian-utils/brutils-python/pull/410)
+- Utilitário `is_holiday` [#446](https://github.com/brazilian-utils/brutils-python/pull/446)
 - Utilitário `convert_date_to_text`[#394](https://github.com/brazilian-utils/brutils-python/pull/415)
 - Utilitário `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412)
 - Utilitário `get_code_by_municipality_name` [#399](https://github.com/brazilian-utils/brutils-python/issues/399)
diff --git a/README.md b/README.md
index 8c35ffd..d1cf8f7 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,8 @@ False
   - [get_code_by_municipality_name](#get_code_by_municipality_name)
   - [convert_code_to_uf](#convert_code_to_uf)
   - [get\_municipality\_by\_code](#get_municipality_by_code)
+- [Feriados](#feriados)
+  - [is_holiday](#is_holiday)
 - [Monetário](#monetário)
   - [format\_currency](#format_currency)
 
@@ -1193,6 +1195,39 @@ None
 None
 ```
 
+## Feriados
+
+### is_holiday
+
+Verifica se uma determinada data é um feriado nacional ou estadual no Brasil.
+
+Esta função recebe um objeto `datetime` como a data e uma UF opcional (Unidade Federativa) para especificar feriados estaduais. Retorna `True` se a data for um feriado, `False` se não for, ou `None` se a data ou UF forem inválidas. Nota: a função não abrange feriados municipais.
+
+Argumentos:
+
+- `date (datetime)`: A data a ser verificada.
+- `uf (str, opcional)`: A abreviação do estado (UF) para verificar feriados estaduais. Se não fornecido, apenas feriados nacionais são considerados.
+
+Retorna:
+
+- `bool | None`: `True` se a data for um feriado, `False` se não for, ou `None` se a data ou UF forem inválidas.
+
+Exemplo:
+
+```python
+>>> from datetime import datetime
+>>> from brutils import is_holiday
+
+>>> is_holiday(datetime(2024, 1, 1))
+True
+>>> is_holiday(datetime(2024, 1, 2))
+False
+>>> is_holiday(datetime(2024, 3, 2), uf="SP")
+False
+>>> is_holiday(datetime(2024, 12, 25), uf="RJ")
+True
+```
+
 ## Monetário
 
 ### format_currency
diff --git a/README_EN.md b/README_EN.md
index 4cd1813..4dd86e7 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -93,6 +93,8 @@ False
   - [convert_code_to_uf](#convert_code_to_uf)
   - [get\_municipality\_by\_code](#get_municipality_by_code)
   - [get_code_by_municipality_name](#get_code_by_municipality_name)
+- [Holidays](#holidays)
+  - [is_holiday](#is_holiday)
 - [Monetary](#monetary)
   - [format_currency](#format_currency)
 
@@ -1197,6 +1199,38 @@ None
 None
 ```
 
+## Holidays
+
+### is_holiday
+
+Checks if a given date is a national or state holiday in Brazil.
+
+This function takes a `datetime` object as the date and an optional state abbreviation (UF) to specify state holidays. It returns `True` if the date is a holiday, `False` if it’s not, or `None` if the date or UF are invalid. Note that the function does not cover municipal holidays.
+
+Args:
+
+- `date (datetime)`: The date to be checked.
+- `uf (str, optional)`: The state abbreviation (UF) to check for state holidays. If not provided, only national holidays are considered.
+
+Returns:
+
+- `bool | None`: `True` if the date is a holiday, `False` if it’s not, or `None` if the date or UF are invalid.
+
+Example:
+
+```python
+>>> from datetime import datetime
+>>> from brutils import is_holiday
+>>> is_holiday(datetime(2024, 1, 1))
+True
+>>> is_holiday(datetime(2024, 1, 2))
+False
+>>> is_holiday(datetime(2024, 3, 2), uf="SP")
+False
+>>> is_holiday(datetime(2024, 12, 25), uf="RJ")
+True
+```
+
 ## Monetary
 
 ### format_currency
diff --git a/brutils/__init__.py b/brutils/__init__.py
index 8717e1d..66b1d2d 100644
--- a/brutils/__init__.py
+++ b/brutils/__init__.py
@@ -26,6 +26,9 @@ from brutils.currency import format_currency
 # Date imports
 from brutils.date import convert_date_to_text
 
+# Date Utils Import
+from brutils.date_utils import is_holiday
+
 # Email Import
 from brutils.email import is_valid as is_valid_email
 
@@ -126,6 +129,8 @@ __all__ = [
     "convert_code_to_uf",
     "get_municipality_by_code",
     "get_code_by_municipality_name",
+    # Date Utils
+    "is_holiday",
     # Currency
     "format_currency",
 ]
diff --git a/brutils/date_utils.py b/brutils/date_utils.py
new file mode 100644
index 0000000..3bada2b
--- /dev/null
+++ b/brutils/date_utils.py
@@ -0,0 +1,58 @@
+from datetime import datetime
+from typing import Union
+
+import holidays
+
+
+def is_holiday(target_date: datetime, uf: str = None) -> Union[bool, None]:
+    """
+    Checks if the given date is a national or state holiday in Brazil.
+
+    This function takes a date as a `datetime` object and an optional UF (Unidade Federativa),
+    returning a boolean value indicating whether the date is a holiday or `None` if the date or
+    UF are invalid.
+
+    The method does not handle municipal holidays.
+
+    Args:
+        target_date (datetime): The date to be checked.
+        uf (str, optional): The state abbreviation (UF) to check for state holidays.
+                            If not provided, only national holidays will be considered.
+
+    Returns:
+        bool | None: Returns `True` if the date is a holiday, `False` if it is not,
+                     or `None` if the date or UF are invalid.
+
+    Note:
+        The function logic should be implemented using the `holidays` library.
+        For more information, refer to the documentation at: https://pypi.org/project/holidays/
+
+    Usage Examples:
+        >>> from datetime import datetime
+        >>> is_holiday(datetime(2024, 1, 1))
+        True
+
+        >>> is_holiday(datetime(2024, 1, 2))
+        False
+
+        >>> is_holiday(datetime(2024, 3, 2), uf="SP")
+        False
+
+        >>> is_holiday(datetime(2024, 12, 25), uf="RJ")
+        True
+    """
+
+    if not isinstance(target_date, datetime):
+        return None
+
+    valid_ufs = holidays.Brazil().subdivisions
+    if uf is not None and uf not in valid_ufs:
+        return None
+
+    national_holidays = holidays.Brazil(years=target_date.year)
+
+    if uf is None:
+        return target_date in national_holidays
+
+    state_holidays = holidays.Brazil(prov=uf, years=target_date.year)
+    return target_date in state_holidays
diff --git a/poetry.lock b/poetry.lock
index fefb6af..1dd578e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -94,6 +94,20 @@ files = [
     {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
 ]
 
+[[package]]
+name = "holidays"
+version = "0.58"
+description = "Generate and work with holidays in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "holidays-0.58-py3-none-any.whl", hash = "sha256:74eb0bd07db862c5b108d5e42cbd33c37ea066a2e8dc54f4858dd3cceef5ee8b"},
+    {file = "holidays-0.58.tar.gz", hash = "sha256:cbe5f3b862474b8f6e5f7ee423f17f3ebd7836fcc74b38540f97b27a7f92dca7"},
+]
+
+[package.dependencies]
+python-dateutil = "*"
+
 [[package]]
 name = "num2words"
 version = "0.5.13"
@@ -108,34 +122,59 @@ files = [
 [package.dependencies]
 docopt = ">=0.6.2"
 
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
 [[package]]
 name = "ruff"
-version = "0.6.7"
+version = "0.6.9"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"},
-    {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"},
-    {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"},
-    {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"},
-    {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"},
-    {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"},
-    {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"},
-    {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"},
-    {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"},
-    {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"},
-    {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"},
-    {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"},
+    {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
+    {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
+    {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
+    {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
+    {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
+    {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
+    {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
+    {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
+    {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
+    {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
+    {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
+    {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+    {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
 ]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8.1"
-content-hash = "957675c81621c16701bae337f25dc9571f3c2a787002725627de366f1b01d16a"
+content-hash = "2af3c2513b328cd5b7640b80a37a81753790caa3fb72784295e01275056f4523"
diff --git a/pyproject.toml b/pyproject.toml
index 7d96716..962d051 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,7 +27,9 @@ classifiers = [
 
 [tool.poetry.dependencies]
 python = "^3.8.1"
+holidays = "^0.58"
 num2words = "0.5.13"
+coverage = "^7.2.7"
 
 [tool.poetry.group.test.dependencies]
 coverage = "^7.2.7"


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index ae5ce19..d3862ba 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -16,10 +16,15 @@ jobs:
         uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
+      - name: Poetry Setup
+        uses: snok/install-poetry@v1
+        with:
+          version: 1.8.4
+      - name: Install Dependencies
+        run: poetry install
       - name: Generate Report
         run: |
-          pip install coverage num2words
-          coverage run -m unittest discover tests/
+          poetry run coverage run -m unittest discover -s tests
       - name: Upload Coverage to Codecov
         uses: codecov/codecov-action@v5
         with:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_date_utils.py b/tests/test_date_utils.py
new file mode 100644
index 0000000..b7350da
--- /dev/null
+++ b/tests/test_date_utils.py
@@ -0,0 +1,75 @@
+from datetime import datetime
+from unittest import TestCase
+
+from brutils.date_utils import is_holiday
+
+
+class TestIsHoliday(TestCase):
+    def test_feriados_validos(self):
+        # Testes com feriados válidos
+        self.assertTrue(is_holiday(datetime(2024, 1, 1)))  # Ano Novo
+        self.assertTrue(
+            is_holiday(datetime(2024, 7, 9), uf="SP")
+        )  # Revolução Constitucionalista (SP)
+        self.assertTrue(
+            is_holiday(datetime(2024, 9, 7))
+        )  # Independência do Brasil
+        self.assertTrue(is_holiday(datetime(2025, 1, 1)))  # Ano Novo
+
+    def test_dias_normais(self):
+        # Testes com dias normais
+        self.assertFalse(is_holiday(datetime(2024, 1, 2)))  # Dia normal
+        self.assertFalse(
+            is_holiday(datetime(2024, 7, 9), uf="RJ")
+        )  # Dia normal no RJ
+
+    def test_data_invalida(self):
+        # Testes com data inválida
+        self.assertIsNone(is_holiday("2024-01-01"))  # Formato incorreto
+        self.assertIsNone(is_holiday(None))  # Data None
+
+    def test_uf_invalida(self):
+        # Testes com UF inválida
+        self.assertIsNone(
+            is_holiday(datetime(2024, 1, 1), uf="XX")
+        )  # UF inválida
+        self.assertIsNone(
+            is_holiday(datetime(2024, 1, 1), uf="SS")
+        )  # UF inválida
+
+    def test_limite_de_datas(self):
+        # Testes com limite de datas
+        self.assertTrue(is_holiday(datetime(2024, 12, 25)))  # Natal
+        self.assertTrue(
+            is_holiday(datetime(2024, 11, 15))
+        )  # Proclamação da República
+
+    def test_datas_depois_de_feriados(self):
+        # Test data after holidays
+        self.assertFalse(is_holiday(datetime(2024, 12, 26)))  # Não é feriado
+        self.assertFalse(is_holiday(datetime(2025, 1, 2)))  # Não é feriado
+
+    def test_ano_bissexto(self):
+        # Teste ano bissexto
+        self.assertFalse(
+            is_holiday(datetime(2024, 2, 29))
+        )  # Não é feriado, mas data válida
+        # Uncomment to test non-leap year invalid date
+        # self.assertIsNone(is_holiday(datetime(1900, 2, 29)))  # Ano não bissexto, data inválida
+
+    def test_data_passada_futura(self):
+        # Teste de data passada e futura
+        self.assertTrue(is_holiday(datetime(2023, 1, 1)))  # Ano anterior
+        self.assertTrue(is_holiday(datetime(2150, 12, 25)))  # Ano futuro
+        self.assertFalse(
+            is_holiday(datetime(2250, 1, 2))
+        )  # Dia normal em ano futuro
+
+    def test_data_sem_uf(self):
+        # Teste feriado nacional sem UF
+        self.assertTrue(
+            is_holiday(datetime(2024, 12, 25))
+        )  # Natal, feriado nacional
+        self.assertFalse(
+            is_holiday(datetime(2024, 7, 9))
+        )  # Data estadual de SP, sem UF