diff --git a/Makefile b/Makefile index a047884..59333eb 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ setup: .PHONY : test test: - pytest + pytest -v .PHONY : tox tox: diff --git a/brazilcep/apicep.py b/brazilcep/apicep.py index eddf345..87e4295 100644 --- a/brazilcep/apicep.py +++ b/brazilcep/apicep.py @@ -23,15 +23,42 @@ def fetch_address(cep: str, timeout: Union[None, int], proxies: Union[None, dict a REST API to query CEP requests. Args: - cep: CEP to be searched. - timeout: How many seconds to wait for the server to return data before giving up. - proxies: Dictionary mapping protocol to the URL of the proxy. + cep: CEP to be searched + timeout: How many seconds to wait for the server to return data before giving up + proxies: Dictionary mapping protocol to the URL of the proxy + + Raises: + exceptions.ConnectionError: raised by a connection error + exceptions.HTTPError: raised by HTTP error + exceptions.URLRequired: raised by using a invalid URL to make a request + exceptions.TooManyRedirects: raised by too many redirects + exceptions.Timeout: raised by request timed out + exceptions.InvalidCEP: raised to invalid CEP requests + exceptions.BlockedByFlood: raised by flood of requests + exceptions.CEPNotFound: raised to CEP not founded requests + exceptions.BrazilCEPException: Base class for exception Returns: - Respective address data from CEP. + Address data from CEP """ - response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + try: + response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + + except requests.exceptions.ConnectionError as exc: + raise exceptions.ConnectionError(exc) + + except requests.exceptions.HTTPError as exc: + raise exceptions.HTTPError(exc) + + except requests.exceptions.URLRequired as exc: + raise exceptions.URLRequired(exc) + + except requests.exceptions.TooManyRedirects as exc: + raise exceptions.TooManyRedirects(exc) + + except requests.exceptions.Timeout as exc: + raise exceptions.Timeout(exc) if response.status_code == 200: address = json.loads(response.text) @@ -54,4 +81,7 @@ def fetch_address(cep: str, timeout: Union[None, int], proxies: Union[None, dict "complement": "", } + elif response.status_code == 429: + raise exceptions.BlockedByFlood() + raise exceptions.BrazilCEPException(f"Other error. Status code: {response.status_code}") diff --git a/brazilcep/client.py b/brazilcep/client.py index 2a6ad0e..618bbb4 100644 --- a/brazilcep/client.py +++ b/brazilcep/client.py @@ -44,26 +44,23 @@ class WebService(enum.Enum): def get_address_from_cep( cep: str, - webservice: WebService = WebService.APICEP, + webservice: WebService = WebService.OPENCEP, timeout: Optional[int] = None, proxies: Optional[dict] = None, ) -> dict: - """Returns the address corresponding to the zip (cep) code entered. + """Returns the address corresponding to the zip (cep) code entered Args: - cep: CEP to be queried. - timeout: How many seconds to wait for the server to return data before giving up. - proxies: Dictionary mapping protocol to the URL of the proxy. + cep: CEP to be queried + webservice: enum to webservice APIs + timeout: How many seconds to wait for the server to return data before giving up + proxies: Dictionary mapping protocol to the URL of the proxy Raises: - RequestError: When connection error occurs in CEP query - Timeout: When occurs timeout of webservice response - HTTPError: Invalid HTTP format query - CEPNotFound: CEP not exist in API - Exception: When any error occurs in the CEP query + KeyError: raise if `webservice` parameter is a invalid webservice enum value - returns: - Address data of the queried CEP. + Returns: + Address data of the queried CEP """ if webservice == WebService.CORREIOS: diff --git a/brazilcep/exceptions.py b/brazilcep/exceptions.py index 3e80f1d..138f4df 100644 --- a/brazilcep/exceptions.py +++ b/brazilcep/exceptions.py @@ -23,3 +23,23 @@ class CEPNotFound(BrazilCEPException): class BlockedByFlood(BrazilCEPException): """Exception raised by flood of requests""" + + +class ConnectionError(BrazilCEPException): + """Exception raised by a connection error""" + + +class HTTPError(BrazilCEPException): + """Exception raised by HTTP error""" + + +class URLRequired(BrazilCEPException): + """Exception raised by using a invalid URL to make a request""" + + +class TooManyRedirects(BrazilCEPException): + """Exception raised by too many redirects""" + + +class Timeout(BrazilCEPException): + """Exception raised by request timed out""" diff --git a/brazilcep/opencep.py b/brazilcep/opencep.py index dd248e6..3b08d09 100644 --- a/brazilcep/opencep.py +++ b/brazilcep/opencep.py @@ -23,15 +23,42 @@ def fetch_address(cep: str, timeout: Union[None, int], proxies: Union[None, dict a REST API to query CEP requests. Args: - cep: CEP to be searched. - timeout: How many seconds to wait for the server to return data before giving up. - proxies: Dictionary mapping protocol to the URL of the proxy. + cep: CEP to be searched + timeout: How many seconds to wait for the server to return data before giving up + proxies: Dictionary mapping protocol to the URL of the proxy + + Raises: + exceptions.ConnectionError: raised by a connection error + exceptions.HTTPError: raised by HTTP error + exceptions.URLRequired: raised by using a invalid URL to make a request + exceptions.TooManyRedirects: raised by too many redirects + exceptions.Timeout: raised by request timed out + exceptions.InvalidCEP: raised to invalid CEP requests + exceptions.BlockedByFlood: raised by flood of requests + exceptions.CEPNotFound: raised to CEP not founded requests + exceptions.BrazilCEPException: Base class for exception Returns: - Respective address data from CEP. + Address data from CEP """ - response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + try: + response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + + except requests.exceptions.ConnectionError as exc: + raise exceptions.ConnectionError(exc) + + except requests.exceptions.HTTPError as exc: + raise exceptions.HTTPError(exc) + + except requests.exceptions.URLRequired as exc: + raise exceptions.URLRequired(exc) + + except requests.exceptions.TooManyRedirects as exc: + raise exceptions.TooManyRedirects(exc) + + except requests.exceptions.Timeout as exc: + raise exceptions.Timeout(exc) if response.status_code == 200: address = json.loads(response.text) diff --git a/brazilcep/viacep.py b/brazilcep/viacep.py index 970d831..262b018 100644 --- a/brazilcep/viacep.py +++ b/brazilcep/viacep.py @@ -23,15 +23,42 @@ def fetch_address(cep: str, timeout: Union[None, int], proxies: Union[None, dict a REST API to query CEP requests. Args: - cep: CEP to be searched. - timeout: How many seconds to wait for the server to return data before giving up. - proxies: Dictionary mapping protocol to the URL of the proxy. + cep: CEP to be searched + timeout: How many seconds to wait for the server to return data before giving up + proxies: Dictionary mapping protocol to the URL of the proxy + + Raises: + exceptions.ConnectionError: raised by a connection error + exceptions.HTTPError: raised by HTTP error + exceptions.URLRequired: raised by using a invalid URL to make a request + exceptions.TooManyRedirects: raised by too many redirects + exceptions.Timeout: raised by request timed out + exceptions.InvalidCEP: raised to invalid CEP requests + exceptions.BlockedByFlood: raised by flood of requests + exceptions.CEPNotFound: raised to CEP not founded requests + exceptions.BrazilCEPException: Base class for exception Returns: - Respective address data from CEP. + Address data from CEP """ - response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + try: + response = requests.get(URL.format(cep), timeout=timeout, proxies=proxies) + + except requests.exceptions.ConnectionError as exc: + raise exceptions.ConnectionError(exc) + + except requests.exceptions.HTTPError as exc: + raise exceptions.HTTPError(exc) + + except requests.exceptions.URLRequired as exc: + raise exceptions.URLRequired(exc) + + except requests.exceptions.TooManyRedirects as exc: + raise exceptions.TooManyRedirects(exc) + + except requests.exceptions.Timeout as exc: + raise exceptions.Timeout(exc) if response.status_code == 200: # Transforma o objeto requests em um dict diff --git a/docs/source/api.rst b/docs/source/api.rst index 49db6ef..03e8c82 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -19,6 +19,11 @@ Exceptions ---------- .. autoexception:: brazilcep.exceptions.BrazilCEPException +.. autoexception:: brazilcep.exceptions.ConnectionError +.. autoexception:: brazilcep.exceptions.HTTPError +.. autoexception:: brazilcep.exceptions.URLRequired +.. autoexception:: brazilcep.exceptions.TooManyRedirects +.. autoexception:: brazilcep.exceptions.Timeout .. autoexception:: brazilcep.exceptions.InvalidCEP .. autoexception:: brazilcep.exceptions.CEPNotFound .. autoexception:: brazilcep.exceptions.BlockedByFlood @@ -72,8 +77,5 @@ This is the new code:: The follow `Exceptions` have been removed: -* `ConnectionError` -* `Timeout` -* `HTTPError` * `BaseException` diff --git a/docs/source/user/quickstart.rst b/docs/source/user/quickstart.rst index a5a5d1a..b007bd4 100644 --- a/docs/source/user/quickstart.rst +++ b/docs/source/user/quickstart.rst @@ -50,12 +50,7 @@ Timeouts You can tell BrazilCEP to stop waiting for a response after a given number of seconds with the ``timeout`` parameter. Nearly all production code should use this parameter in nearly all requests. Failure to do so can cause your program -to hang indefinitely:: - - >>> get_address_from_cep('37503-130', timeout=0.001) - Traceback (most recent call last): - File "", line 1, in - requests.exceptions.Timeout: HTTPConnectionPool(host='http://www.viacep.com.br/ws/37503130/json', port=80): Request timed out. (timeout=0.001) +to hang indefinitely. Proxy ----- @@ -87,7 +82,7 @@ Unsing differents API's BrazilCEP is not responsible for the functioning, availability and support of any of these query API's. All of them are provided by third parties, and this library just provides a handy way to centralize the CEP search on these services. -By default, BrazilCEP uses the API provided by the `ApiCEP `_ service. +By default, BrazilCEP uses the API provided by the `OpenCEP `_ service. To use other services, we must indicate the desired service when calling the `get_address_from_cep` function. @@ -120,13 +115,21 @@ Errors and Exceptions BrazilCEP also supports a group of exceptions that can be used to handle any errors that occur during the query process. -If a invalid CEP request has be made, a :exc:`~brazilcep.exceptions.InvalidCEP` exception is -raised. +:exc:`~brazilcep.exceptions.InvalidCEP` exception raised by a request with invalid CEP. + +:exc:`~brazilcep.exceptions.CEPNotFound` exception is raised when CEP is not find in selected API. + +:exc:`~brazilcep.exceptions.BlockedByFlood`: exception raides by a large number of CEP requests in short range of time + +:exc:`~brazilcep.exceptions.ConnectionError`: exception raised by a connection error. + +:exc:`~brazilcep.exceptions.HTTPError`: exception raised by HTTP error. + +:exc:`~brazilcep.exceptions.URLRequired`: exception raised by using a invalid URL to make a CEP request. -If a CEP request not find CEP address, a :exc:`~brazilcep.exceptions.CEPNotFound` exception is raised. +:exc:`~brazilcep.exceptions.TooManyRedirects`: Exception raised by too many redirects. -To a large number of CEP requests in short range of time, a :exc:`~brazilcep.exceptions.BlockedByFlood` exception is raised. +:exc:`~brazilcep.exceptions.Timeout`: exception raised by request timed out. -All exceptions that BrazilCEP explicitly raises inherit from -:exc:`brazilcep.exceptions.BrazilCEPException`. +All exceptions that BrazilCEP explicitly raises inherit from :exc:`brazilcep.exceptions.BrazilCEPException`. diff --git a/pyproject.toml b/pyproject.toml index 56ea48f..d11cf62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,17 +117,7 @@ skip = ["docs"] [tool.pyright] reportPrivateImportUsage = false -[tool.ruff] -line-length = 115 -target-version = "py39" - -[tool.lint.per-file-ignores] -"__init__.py" = ["F401"] - [tool.mypy] -# ignore_missing_imports = true -# no_site_packages = true -# check_untyped_defs = true strict = false [[tool.mypy.overrides]] diff --git a/tests/test_apicep.py b/tests/test_apicep.py index fd0a37c..a6eeb01 100644 --- a/tests/test_apicep.py +++ b/tests/test_apicep.py @@ -1,11 +1,12 @@ import os +import dotenv import pytest -from dotenv import load_dotenv +import requests from brazilcep import WebService, exceptions, get_address_from_cep -load_dotenv() +dotenv.load_dotenv() IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" SKIP_REAL_TEST = os.getenv("SKIP_REAL_TEST", True) @@ -24,6 +25,13 @@ def test_fetch_address_success_real(): assert address["uf"] == "MG" +@pytest.mark.skipif(SKIP_REAL_TEST, reason="Skip real teste API.") +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") +def test_fetch_address_cep_not_found_real(): + with pytest.raises(exceptions.InvalidCEP): + get_address_from_cep("37.503-13", webservice=WebService.APICEP) + + def test_fetch_address_success(requests_mock): req_mock_text = """{ "status":200, @@ -98,15 +106,22 @@ def test_fetch_address_invalid_cep(requests_mock): def test_fetch_address_blocked_by_flood(requests_mock): - req_mock_text = """{ + req_mock_text_400 = """{ "status":400, "message": "Blocked by flood" }""" - requests_mock.get("https://ws.apicep.com/cep/3750313.json", text=req_mock_text) + requests_mock.get("https://ws.apicep.com/cep/37503130.json", text=req_mock_text_400) with pytest.raises(exceptions.BlockedByFlood): - get_address_from_cep("37503-13", webservice=WebService.APICEP) + get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_fetch_address_429(requests_mock): + requests_mock.get("https://ws.apicep.com/cep/37503130.json", status_code=429) + + with pytest.raises(exceptions.BlockedByFlood): + get_address_from_cep("37503-130", webservice=WebService.APICEP) def test_fetch_address_404(requests_mock): @@ -114,3 +129,44 @@ def test_fetch_address_404(requests_mock): with pytest.raises(exceptions.BrazilCEPException): get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_connection_error(requests_mock): + requests_mock.get( + "https://ws.apicep.com/cep/37503130.json", exc=requests.exceptions.ConnectionError + ) + + with pytest.raises(exceptions.ConnectionError): + get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_http_error(requests_mock): + requests_mock.get("https://ws.apicep.com/cep/37503130.json", exc=requests.exceptions.HTTPError) + + with pytest.raises(exceptions.HTTPError): + get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_url_required_error(requests_mock): + requests_mock.get( + "https://ws.apicep.com/cep/37503130.json", exc=requests.exceptions.URLRequired + ) + + with pytest.raises(exceptions.URLRequired): + get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_too_many_redirects_error(requests_mock): + requests_mock.get( + "https://ws.apicep.com/cep/37503130.json", exc=requests.exceptions.TooManyRedirects + ) + + with pytest.raises(exceptions.TooManyRedirects): + get_address_from_cep("37503-130", webservice=WebService.APICEP) + + +def test_timeout_error(requests_mock): + requests_mock.get("https://ws.apicep.com/cep/37503130.json", exc=requests.exceptions.Timeout) + + with pytest.raises(exceptions.Timeout): + get_address_from_cep("37503-130", webservice=WebService.APICEP) diff --git a/tests/test_opencep.py b/tests/test_opencep.py index 2fa7fd3..6a2bca6 100644 --- a/tests/test_opencep.py +++ b/tests/test_opencep.py @@ -1,11 +1,13 @@ import os +import dotenv import pytest -from dotenv import load_dotenv +import requests from brazilcep import WebService, exceptions, get_address_from_cep -load_dotenv() +dotenv.load_dotenv() + IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" SKIP_REAL_TEST = os.getenv("SKIP_REAL_TEST", True) @@ -23,6 +25,13 @@ def test_fetch_address_success_real(): assert address["uf"] == "MG" +@pytest.mark.skipif(SKIP_REAL_TEST, reason="Skip real teste API.") +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") +def test_fetch_address_cep_not_found_real(): + with pytest.raises(exceptions.CEPNotFound): + get_address_from_cep("00000-000", webservice=WebService.OPENCEP) + + def test_fetch_address_success(requests_mock): req_mock_text = """{ "cep": "37503-130", @@ -84,3 +93,38 @@ def test_fetch_address_500(requests_mock): with pytest.raises(exceptions.BrazilCEPException): get_address_from_cep("37503-130", webservice=WebService.OPENCEP) + + +def test_connection_error(requests_mock): + requests_mock.get("https://opencep.com/v1/37503130", exc=requests.exceptions.ConnectionError) + + with pytest.raises(exceptions.ConnectionError): + get_address_from_cep("37503-130", webservice=WebService.OPENCEP) + + +def test_http_error(requests_mock): + requests_mock.get("https://opencep.com/v1/37503130", exc=requests.exceptions.HTTPError) + + with pytest.raises(exceptions.HTTPError): + get_address_from_cep("37503-130", webservice=WebService.OPENCEP) + + +def test_url_required_error(requests_mock): + requests_mock.get("https://opencep.com/v1/37503130", exc=requests.exceptions.URLRequired) + + with pytest.raises(exceptions.URLRequired): + get_address_from_cep("37503-130", webservice=WebService.OPENCEP) + + +def test_too_many_redirects_error(requests_mock): + requests_mock.get("https://opencep.com/v1/37503130", exc=requests.exceptions.TooManyRedirects) + + with pytest.raises(exceptions.TooManyRedirects): + get_address_from_cep("37503-130", webservice=WebService.OPENCEP) + + +def test_timeout_error(requests_mock): + requests_mock.get("https://opencep.com/v1/37503130", exc=requests.exceptions.Timeout) + + with pytest.raises(exceptions.Timeout): + get_address_from_cep("37503-130", webservice=WebService.OPENCEP) diff --git a/tests/test_viacep.py b/tests/test_viacep.py index fe204bc..7240911 100644 --- a/tests/test_viacep.py +++ b/tests/test_viacep.py @@ -1,15 +1,16 @@ import logging import os +import dotenv import pytest import requests -from dotenv import load_dotenv from brazilcep import WebService, exceptions, get_address_from_cep logger = logging.getLogger(__name__) -load_dotenv() +dotenv.load_dotenv() + IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" SKIP_REAL_TEST = os.getenv("SKIP_REAL_TEST", False) @@ -17,18 +18,21 @@ @pytest.mark.skipif(SKIP_REAL_TEST, reason="Skip real teste API.") @pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") def test_get_address_from_cep_success_real(): - try: - address = get_address_from_cep("37.503-130", webservice=WebService.VIACEP) + address = get_address_from_cep("37.503-130", webservice=WebService.VIACEP) - assert address["district"] == "Santo Antônio" - assert address["cep"] == "37503-130" - assert address["city"] == "Itajubá" - assert address["complement"] == "até 214/215" - assert address["street"] == "Rua Geraldino Campista" - assert address["uf"] == "MG" + assert address["district"] == "Santo Antônio" + assert address["cep"] == "37503-130" + assert address["city"] == "Itajubá" + assert address["complement"] == "até 214/215" + assert address["street"] == "Rua Geraldino Campista" + assert address["uf"] == "MG" - except requests.exceptions.ConnectionError as exc: - logger.warning(exc) + +@pytest.mark.skipif(SKIP_REAL_TEST, reason="Skip real teste API.") +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.") +def test_get_address_from_cep_not_found_real(): + with pytest.raises(exceptions.CEPNotFound): + get_address_from_cep("00000-000", webservice=WebService.VIACEP) def test_get_address_from_cep_success(requests_mock): @@ -91,3 +95,46 @@ def test_fetch_address_404(requests_mock): with pytest.raises(exceptions.BrazilCEPException): get_address_from_cep("37503-130", webservice=WebService.VIACEP) + + +def test_connection_error(requests_mock): + requests_mock.get( + "http://www.viacep.com.br/ws/37503130/json", exc=requests.exceptions.ConnectionError + ) + + with pytest.raises(exceptions.ConnectionError): + get_address_from_cep("37503-130", webservice=WebService.VIACEP) + + +def test_http_error(requests_mock): + requests_mock.get( + "http://www.viacep.com.br/ws/37503130/json", exc=requests.exceptions.HTTPError + ) + + with pytest.raises(exceptions.HTTPError): + get_address_from_cep("37503-130", webservice=WebService.VIACEP) + + +def test_url_required_error(requests_mock): + requests_mock.get( + "http://www.viacep.com.br/ws/37503130/json", exc=requests.exceptions.URLRequired + ) + + with pytest.raises(exceptions.URLRequired): + get_address_from_cep("37503-130", webservice=WebService.VIACEP) + + +def test_too_many_redirects_error(requests_mock): + requests_mock.get( + "http://www.viacep.com.br/ws/37503130/json", exc=requests.exceptions.TooManyRedirects + ) + + with pytest.raises(exceptions.TooManyRedirects): + get_address_from_cep("37503-130", webservice=WebService.VIACEP) + + +def test_timeout_error(requests_mock): + requests_mock.get("http://www.viacep.com.br/ws/37503130/json", exc=requests.exceptions.Timeout) + + with pytest.raises(exceptions.Timeout): + get_address_from_cep("37503-130", webservice=WebService.VIACEP)