diff --git a/CHANGELOG.md b/CHANGELOG.md index b1546edc..9650cc14 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 73769b04..be1a52ac 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 7917b673..58a24862 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 6959a0d0..fc7f35a3 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 00000000..c3ffb7ff --- /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 diff --git a/tests/ibge/test_municipality.py b/tests/ibge/test_municipality.py new file mode 100644 index 00000000..516febb3 --- /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()