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

Issue:
Retornar Município e UF a partir do Código IBGE
**Seu pedido de recurso está relacionado a um problema? Por favor, descreva.**

Dado um código de município do IBGE em formato de string, quero obter uma tupla com o nome do município e a UF correspondente.

Por exemplo, ao passar o código `"3550308"`, a função deve retornar `("São Paulo", "SP")`.

**Descreva a solução que você gostaria**

* Uma função `get_municipality_by_code`, que recebe um código de município (string) e retorna uma tupla com o nome do município e a UF correspondente.
* A função deve verificar a validade do código, ou seja, se ele existe na base de dados do IBGE.
* Para códigos inválidos, a função deve retornar `None`.
* A função pode usar uma base de dados local contendo os códigos, municípios e UFs ou consultar uma API externa que forneça essa informação.
* A saída deve ser formatada como: `("Município", "UF")`. Exemplo: `("São Paulo", "SP")`.
* Um enum das UFs e seus nomes completos já existe no arquivo `brutils/data/enums/uf.py`. Ele deve ser reutilizado.

**Descreva alternativas que você considerou**

1. Seguir até o passo 8 do [guia de contribuição][guia-de-contribuição].

1. Como parte do passo 8, criar o arquivo: `brutils-python/brutils/ibge/municipality.py`.

    ```python
    def get_municipality_by_code(code): # type: (str) -> (str, str) | None
        """
        Returns the municipality name and UF for a given IBGE code.

        This function takes a string representing an IBGE municipality code 
        and returns a tuple with the municipality's name and its corresponding UF.

        Args:
            code (str): The IBGE code of the municipality.

        Returns:
            tuple: A tuple formatted as ("Município", "UF").
                - Returns None if the code is not valid.

        Example:
            >>> get_municipality_by_code("3550308")
            ("São Paulo", "SP")
        """
        # Implementar a lógica para recuperar o município e a UF pelo código do IBGE
    ```

    Importar a nova função no arquivo `brutils-python/brutils/__init__.py`

    ```python
    # IBGE Imports    
    from brutils.ibge.municipality import (
        get_municipality_by_code,
    )
    ```

    E adicionar o nome da nova função na lista `__all__` do mesmo arquivo `brutils-python/brutils/__init__.py`:

    ```python
    __all__ = [
        ...
        # IBGE
        get_municipality_by_code
    ]
    ```

1. Como parte do passo 9, criar o arquivo de teste: `brutils-python/tests/ibge/municipality.py`.

    ```python
    from unittest import TestCase
    from brutils.ibge.municipality import get_municipality_by_code

    class TestIBGE(TestCase):
        def test_get_municipality_by_code(self):
            self.assertEqual(get_municipality_by_code("3550308"), ("São Paulo", "SP"))
            self.assertEqual(get_municipality_by_code("3304557"), ("Rio de Janeiro", "RJ"))
            self.assertEqual(get_municipality_by_code("5208707"), ("Goiânia", "GO"))
            self.assertIsNone(get_municipality_by_code("1234567"))

            # implementar mais casos de teste aqui se necessário
    ```

1. Seguir os passos seguintes do [guia de contribuição][guia-de-contribuição].

**Contexto adicional**

* A função deve ser capaz de validar se o código IBGE está correto e se corresponde a um município existente.
* O código IBGE deve ser passado como uma string de sete dígitos.
* Referência de códigos IBGE: https://www.ibge.gov.br/explica/codigos-dos-municipios.php

[guia-de-contribuição]: https://github.com/brazilian-utils/brutils-python/blob/main/CONTRIBUTING.md#primeira-contribui%C3%A7%C3%A3o


Discussion / hints:
Olá, posso trabalhar nessa issue?
Feito! @jaimenunes 

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1546ed..9650cc1 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 `get_municipality_by_code` [412](https://github.com/brazilian-utils/brutils-python/pull/412)
 
 ## [2.2.0] - 2024-09-12
 
diff --git a/README.md b/README.md
index 73769b0..be1a52a 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,7 @@ False
   - [generate_voter_id](#generate_voter_id)
 - [IBGE](#ibge)
   - [convert_code_to_uf](#convert_code_to_uf)
+  - [get\_municipality\_by\_code](#get_municipality_by_code)
 
 ## CPF
 
@@ -1087,6 +1088,7 @@ Exemplo:
 ```
 
 ## IBGE
+
 ### convert_code_to_uf
 Converte um determinado código do IBGE (string de 2 dígitos) para sua UF (abreviatura estadual) correspondente.
 
@@ -1109,6 +1111,24 @@ Exemplo:
 >>>
 ```
 
+### get_municipality_by_code
+
+Retorna o nome do município e a UF para um código do IBGE.
+
+Args:
+  * code (str): O código do IBGE para o município.
+
+Returns:
+  * tuple: Retorna uma Tupla formatado como ("Município", "UF").
+  * None: Retorna None se o código for inválido.
+
+Example:
+
+```python
+>>> from brutils import get_municipality_by_code
+>>> get_municipality_by_code(3550308)
+("São Paulo", "SP")
+```
 
 # Novos Utilitários e Reportar Bugs
 
diff --git a/README_EN.md b/README_EN.md
index 7917b67..58a2486 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -89,6 +89,7 @@ False
   - [generate_voter_id](#generate_voter_id)
 - [IBGE](#ibge)
   - [convert_code_to_uf](#convert_code_to_uf)
+  - [get\_municipality\_by\_code](#get_municipality_by_code)
 
 ## CPF
 
@@ -1088,8 +1089,8 @@ Example:
 >>> generate_voter_id(federative_union ="MG")
 '950125640248'
 ```
-
 ## IBGE
+
 ### convert_code_to_uf
 Converts a given IBGE code (2-digit string) to its corresponding UF (state abbreviation).
 
@@ -1112,6 +1113,25 @@ Exemplo:
 >>>
 ```
 
+### get_municipality_by_code
+
+Returns the municipality name and UF for a given IBGE code.
+
+Args:
+  * code (str): The IBGE code of the municipality.
+
+Returns:
+  * tuple: Returns a tuple formatted as ("Município", "UF").
+  * None: Returns None if the code is not valid.
+
+Example:
+
+```python
+>>> from brutils import get_municipality_by_code
+>>> get_municipality_by_code(3550308)
+("São Paulo", "SP")
+```
+
 # Feature Request and Bug Report
 
 If you want to suggest new features or report bugs, simply create
diff --git a/brutils/__init__.py b/brutils/__init__.py
index 6959a0d..fc7f35a 100644
--- a/brutils/__init__.py
+++ b/brutils/__init__.py
@@ -44,6 +44,9 @@
 
 # Email Import
 from brutils.email import is_valid as is_valid_email
+from brutils.ibge.municipality import (
+    get_municipality_by_code,
+)
 
 # IBGE Imports
 from brutils.ibge.uf import (
@@ -172,4 +175,5 @@
     "is_valid_voter_id",
     # IBGE
     "convert_code_to_uf",
+    "get_municipality_by_code",
 ]
diff --git a/brutils/ibge/municipality.py b/brutils/ibge/municipality.py
new file mode 100644
index 0000000..c3ffb7f
--- /dev/null
+++ b/brutils/ibge/municipality.py
@@ -0,0 +1,81 @@
+import gzip
+import io
+import json
+from urllib.error import HTTPError
+from urllib.request import urlopen
+
+
+def get_municipality_by_code(code):  # type: (str) -> Tuple[str, str] | None
+    """
+    Returns the municipality name and UF for a given IBGE code.
+
+    This function takes a string representing an IBGE municipality code
+    and returns a tuple with the municipality's name and its corresponding UF.
+
+    Args:
+        code (str): The IBGE code of the municipality.
+
+    Returns:
+        tuple: A tuple formatted as ("Município", "UF").
+            - Returns None if the code is not valid.
+
+    Example:
+        >>> get_municipality_by_code("3550308")
+        ("São Paulo", "SP")
+    """
+    baseUrl = (
+        f"https://servicodados.ibge.gov.br/api/v1/localidades/municipios/{code}"
+    )
+    try:
+        with urlopen(baseUrl) as f:
+            compressed_data = f.read()
+            if f.info().get("Content-Encoding") == "gzip":
+                try:
+                    with gzip.GzipFile(
+                        fileobj=io.BytesIO(compressed_data)
+                    ) as gzip_file:
+                        decompressed_data = gzip_file.read()
+                except OSError as e:
+                    print(f"Erro ao descomprimir os dados: {e}")
+                    return None
+                except Exception as e:
+                    print(f"Erro desconhecido ao descomprimir os dados: {e}")
+                    return None
+            else:
+                decompressed_data = compressed_data
+
+            if _is_empty(decompressed_data):
+                print(f"{code} é um código inválido")
+                return None
+
+    except HTTPError as e:
+        if e.code == 404:
+            print(f"{code} é um código inválido")
+            return None
+        else:
+            print(f"Erro HTTP ao buscar o código {code}: {e}")
+            return None
+
+    except Exception as e:
+        print(f"Erro desconhecido ao buscar o código {code}: {e}")
+        return None
+
+    try:
+        json_data = json.loads(decompressed_data)
+        return _get_values(json_data)
+    except json.JSONDecodeError as e:
+        print(f"Erro ao decodificar os dados JSON: {e}")
+        return None
+    except KeyError as e:
+        print(f"Erro ao acessar os dados do município: {e}")
+        return None
+
+
+def _get_values(data):
+    municipio = data["nome"]
+    estado = data["microrregiao"]["mesorregiao"]["UF"]["sigla"]
+    return (municipio, estado)
+
+
+def _is_empty(zip):
+    return zip == b"[]" or len(zip) == 0


Test patch:
diff --git a/tests/ibge/test_municipality.py b/tests/ibge/test_municipality.py
new file mode 100644
index 0000000..516febb
--- /dev/null
+++ b/tests/ibge/test_municipality.py
@@ -0,0 +1,90 @@
+import gzip
+from json import JSONDecodeError
+from unittest import TestCase, main
+from unittest.mock import MagicMock, patch
+from urllib.error import HTTPError
+
+from brutils.ibge.municipality import get_municipality_by_code
+
+
+class TestIBGE(TestCase):
+    def test_get_municipality_by_code(self):
+        self.assertEqual(
+            get_municipality_by_code("3550308"), ("São Paulo", "SP")
+        )
+        self.assertEqual(
+            get_municipality_by_code("3304557"), ("Rio de Janeiro", "RJ")
+        )
+        self.assertEqual(get_municipality_by_code("5208707"), ("Goiânia", "GO"))
+        self.assertIsNone(get_municipality_by_code("1234567"))
+
+    @patch("brutils.ibge.municipality.urlopen")
+    def test_get_municipality_http_error(self, mock):
+        mock.side_effect = HTTPError(
+            "http://fakeurl.com", 404, "Not Found", None, None
+        )
+        result = get_municipality_by_code("342432")
+        self.assertIsNone(result)
+
+    @patch("brutils.ibge.municipality.urlopen")
+    def test_get_municipality_http_error_1(self, mock):
+        mock.side_effect = HTTPError(
+            "http://fakeurl.com", 401, "Denied", None, None
+        )
+        result = get_municipality_by_code("342432")
+        self.assertIsNone(result)
+
+    @patch("brutils.ibge.municipality.urlopen")
+    def test_get_municipality_excpetion(self, mock):
+        mock.side_effect = Exception("Erro desconhecido")
+        result = get_municipality_by_code("342432")
+        self.assertIsNone(result)
+
+    @patch("brutils.ibge.municipality.urlopen")
+    def test_successfull_decompression(self, mock_urlopen):
+        valid_json = '{"nome":"São Paulo","microrregiao":{"mesorregiao":{"UF":{"sigla":"SP"}}}}'
+        compressed_data = gzip.compress(valid_json.encode("utf-8"))
+        mock_response = MagicMock()
+        mock_response.read.return_value = compressed_data
+        mock_response.info.return_value.get.return_value = "gzip"
+        mock_urlopen.return_value.__enter__.return_value = mock_response
+
+        result = get_municipality_by_code("3550308")
+        self.assertEqual(result, ("São Paulo", "SP"))
+
+    @patch("brutils.ibge.municipality.urlopen")
+    def test_successful_json_without_compression(self, mock_urlopen):
+        valid_json = '{"nome":"São Paulo","microrregiao":{"mesorregiao":{"UF":{"sigla":"SP"}}}}'
+        mock_response = MagicMock()
+        mock_response.read.return_value = valid_json
+        mock_urlopen.return_value.__enter__.return_value = mock_response
+
+        result = get_municipality_by_code("3550308")
+        self.assertEqual(result, ("São Paulo", "SP"))
+
+    @patch("gzip.GzipFile.read", side_effect=OSError("Erro na descompressão"))
+    def test_error_decompression(self, mock_gzip_read):
+        result = get_municipality_by_code("3550308")
+        self.assertIsNone(result)
+
+    @patch(
+        "gzip.GzipFile.read",
+        side_effect=Exception("Erro desconhecido na descompressão"),
+    )
+    def test_error_decompression_generic_exception(self, mock_gzip_read):
+        result = get_municipality_by_code("3550308")
+        self.assertIsNone(result)
+
+    @patch("json.loads", side_effect=JSONDecodeError("error", "city.json", 1))
+    def test_error_json_load(self, mock_json_loads):
+        result = get_municipality_by_code("3550308")
+        self.assertIsNone(result)
+
+    @patch("json.loads", side_effect=KeyError)
+    def test_error_json_key_error(self, mock_json_loads):
+        result = get_municipality_by_code("3550308")
+        self.assertIsNone(result)
+
+
+if __name__ == "__main__":
+    main()