1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
"""Exception hierarchy.
Everything derives from :class:`CepisError`, so callers who only care that
*something* went wrong can catch one type.
Below that, the split is by **the decision the caller has to make**, not by the
HTTP status that caused it:
- :class:`InvalidCEP` and :class:`CEPNotFound` are problems with the *request*.
Retrying is pointless — the same CEP will fail again.
- :class:`NetworkError`, :class:`ServerError` and :class:`RateLimited` are
problems with *transport*. Retrying may help, and the client already retried
on its own before raising any of them.
That split is why `CEPNotFound` is not "the HTTP 404 exception": the client
also raises it without making any request at all, when the CEP is malformed
before it ever leaves the machine.
"""
from __future__ import annotations
from .models import Faixa
class CepisError(Exception):
"""Base of every error. Catch this to handle any failure."""
class InvalidCEP(CepisError, ValueError):
"""The CEP is not 8 digits.
Deliberately also a :class:`ValueError`: this is an argument error, and
code that already handles `ValueError` while validating input keeps
working without knowing this library exists.
Usually raised **without** a request ever being made — the client checks
the format before going to the network.
"""
class CEPNotFound(CepisError, KeyError):
"""The CEP is well-formed but does not exist in the DNE database.
Also a :class:`KeyError`, by the same reasoning as `InvalidCEP`: looking a
CEP up is conceptually a key lookup.
`KeyError.__str__` wraps the message in quotes (inherited from dict
behaviour), which is why `__str__` is overridden here — otherwise every
message would come out with spurious quotes around it.
`faixa` is the municipality the CEP *would* have belonged to, when the API
could work it out: see :class:`cepis.Faixa`. It is `None` whenever the API
did not say — either because the CEP falls in a band no municipality owns,
or because the server is older than this field. Either way the exception is
the same exception: `faixa` is context for the error, never a demotion of
it into a partial success.
"""
def __init__(self, message: str, faixa: Faixa | None = None) -> None:
super().__init__(message)
self.faixa = faixa
def __str__(self) -> str:
return self.args[0] if self.args else ""
class NetworkError(CepisError):
"""The request never completed: DNS, refused connection, timeout, TLS.
Raised only after every configured attempt has failed.
"""
class ServerError(CepisError):
"""The API answered 5xx on every attempt.
`status` holds the HTTP status seen on the last attempt.
"""
def __init__(self, message: str, status: int) -> None:
super().__init__(message)
self.status = status
class RateLimited(CepisError):
"""The API (or the edge in front of it) answered 429.
`retry_after` is what the server asked for, in seconds, or `None` if it
did not say. The client already honoured that value across the attempts it
made; it is exposed here for callers who want to reschedule the work.
"""
def __init__(self, message: str, retry_after: float | None = None) -> None:
super().__init__(message)
self.retry_after = retry_after
class InvalidResponse(CepisError):
"""A success status came back with a body that cannot be parsed.
In practice this means something between you and the API is answering
instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error
page. Useful as a diagnosis: the problem is not in your code.
"""
|