getrak-python-sdk 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- getrak/api.py +146 -41
- getrak/apis/recebidos.py +33 -2
- getrak/errors.py +14 -0
- {getrak_python_sdk-1.0.0.dist-info → getrak_python_sdk-1.2.0.dist-info}/METADATA +3 -1
- {getrak_python_sdk-1.0.0.dist-info → getrak_python_sdk-1.2.0.dist-info}/RECORD +8 -8
- {getrak_python_sdk-1.0.0.dist-info → getrak_python_sdk-1.2.0.dist-info}/WHEEL +1 -1
- getrak_python_sdk-1.2.0.dist-info/top_level.txt +1 -0
- __init__.py +0 -0
- getrak_python_sdk-1.0.0.dist-info/top_level.txt +0 -2
- {getrak_python_sdk-1.0.0.dist-info → getrak_python_sdk-1.2.0.dist-info}/licenses/LICENSE +0 -0
getrak/api.py
CHANGED
|
@@ -1,46 +1,151 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
2
6
|
from http import HTTPMethod
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
from fmconsult.exceptions.bad_request_exception import BadRequestException
|
|
9
|
+
from fmconsult.exceptions.forbidden_exception import ForbiddenException
|
|
10
|
+
from fmconsult.exceptions.unauthorized_exception import UnauthorizedException
|
|
11
|
+
from fmconsult.http.api import ApiBase, ContentType
|
|
4
12
|
from fmconsult.utils.url import UrlUtil
|
|
5
13
|
|
|
14
|
+
from getrak.errors import AuthenticationFailed, GetrakError
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
AUTH_REJECTED = (BadRequestException, UnauthorizedException, ForbiddenException)
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
class GetrakApi(ApiBase):
|
|
22
|
+
"""Cliente da API Getrak.
|
|
23
|
+
|
|
24
|
+
Sem argumentos, lê `getrak.api.auth_code`, `getrak.api.login`, `getrak.api.password`
|
|
25
|
+
e `getrak.api.grant_type` e autentica na construção, como antes. Usuário e senha vão
|
|
26
|
+
no corpo do POST. Os demais argumentos são opcionais e servem para reutilizar um
|
|
27
|
+
token, limitar a taxa e tentar de novo em falha passageira.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
auth_code=None,
|
|
34
|
+
login=None,
|
|
35
|
+
password=None,
|
|
36
|
+
grant_type=None,
|
|
37
|
+
base_url=None,
|
|
38
|
+
rate_limiter=None,
|
|
39
|
+
retry_transient=False,
|
|
40
|
+
authenticate_on_init=True,
|
|
41
|
+
verbose=True,
|
|
42
|
+
sleep=None,
|
|
43
|
+
clock=None,
|
|
44
|
+
max_attempts=5,
|
|
45
|
+
on_rate_limited=None,
|
|
46
|
+
on_token=None,
|
|
47
|
+
log_requests=True,
|
|
48
|
+
timeout=60,
|
|
49
|
+
):
|
|
50
|
+
self.api_auth_code = auth_code if auth_code is not None else os.environ["getrak.api.auth_code"]
|
|
51
|
+
self.api_login = login if login is not None else os.environ["getrak.api.login"]
|
|
52
|
+
self.api_password = password if password is not None else os.environ["getrak.api.password"]
|
|
53
|
+
self.api_grant_type = grant_type if grant_type is not None else os.environ["getrak.api.grant_type"]
|
|
54
|
+
self.base_url = (base_url or "https://api.getrak.com").rstrip("/")
|
|
55
|
+
self.verbose = verbose
|
|
56
|
+
self._rate_limiter = rate_limiter
|
|
57
|
+
self._retry_transient = retry_transient
|
|
58
|
+
self._max_attempts = max_attempts
|
|
59
|
+
self._sleep = sleep or time.sleep
|
|
60
|
+
self._clock = clock or time.monotonic
|
|
61
|
+
self._on_rate_limited = on_rate_limited
|
|
62
|
+
self._on_token = on_token
|
|
63
|
+
self._log_requests = log_requests
|
|
64
|
+
self._timeout = timeout
|
|
65
|
+
self._auth_failed = False
|
|
66
|
+
self._token = None
|
|
67
|
+
self._token_type = "Bearer"
|
|
68
|
+
self._token_lock = threading.Lock()
|
|
69
|
+
self.requests = 0
|
|
70
|
+
self.rate_limited = 0
|
|
71
|
+
self.last_http_attempts = 0
|
|
72
|
+
self.current_operation = None
|
|
73
|
+
self.headers = {"Authorization": f"Basic {self.api_auth_code}"}
|
|
74
|
+
if authenticate_on_init:
|
|
75
|
+
self._ensure_token()
|
|
76
|
+
|
|
77
|
+
def _raise_if_auth_failed(self):
|
|
78
|
+
if self._auth_failed:
|
|
79
|
+
raise AuthenticationFailed("autenticação na Getrak já foi recusada nesta execução")
|
|
80
|
+
|
|
81
|
+
def _ensure_token(self):
|
|
82
|
+
with self._token_lock:
|
|
83
|
+
self._raise_if_auth_failed()
|
|
84
|
+
if self._token is None:
|
|
85
|
+
self._token = self._authenticate()
|
|
86
|
+
return self._token
|
|
87
|
+
|
|
88
|
+
def _renew_token(self, stale):
|
|
89
|
+
with self._token_lock:
|
|
90
|
+
self._raise_if_auth_failed()
|
|
91
|
+
if self._token is None or self._token == stale:
|
|
92
|
+
self._token = self._authenticate()
|
|
93
|
+
return self._token
|
|
94
|
+
|
|
95
|
+
def _authenticate(self):
|
|
96
|
+
if self.verbose:
|
|
97
|
+
log.info("api authentication...")
|
|
98
|
+
self.current_operation = "autenticação"
|
|
99
|
+
self.headers = {"Authorization": f"Basic {self.api_auth_code}"}
|
|
100
|
+
url = UrlUtil().make_url(self.base_url, ["newkoauth", "oauth", "token"])
|
|
101
|
+
try:
|
|
102
|
+
raw = self.call_request(
|
|
103
|
+
HTTPMethod.POST,
|
|
104
|
+
url,
|
|
105
|
+
payload={
|
|
106
|
+
"username": self.api_login,
|
|
107
|
+
"password": self.api_password,
|
|
108
|
+
"grant_type": self.api_grant_type,
|
|
109
|
+
},
|
|
110
|
+
content_type=ContentType.FORM_URLENCODED,
|
|
111
|
+
request_timeout=self._timeout,
|
|
112
|
+
)
|
|
113
|
+
except AUTH_REJECTED:
|
|
114
|
+
self._auth_failed = True
|
|
115
|
+
raise AuthenticationFailed(
|
|
116
|
+
"Getrak recusou a autenticação; verifique usuário, senha e a credencial do cliente"
|
|
117
|
+
) from None
|
|
118
|
+
try:
|
|
119
|
+
data = json.loads(raw)
|
|
120
|
+
token = data["access_token"]
|
|
121
|
+
except (ValueError, KeyError, TypeError):
|
|
122
|
+
raise GetrakError("resposta de autenticação da Getrak sem access_token") from None
|
|
123
|
+
if not isinstance(token, str) or not token:
|
|
124
|
+
raise GetrakError("resposta de autenticação da Getrak sem access_token")
|
|
125
|
+
if isinstance(data, dict) and data.get("error"):
|
|
126
|
+
raise AuthenticationFailed("Getrak recusou a autenticação")
|
|
127
|
+
token_type = str(data.get("token_type") or "Bearer")
|
|
128
|
+
self._token_type = "Bearer" if token_type.lower() == "bearer" else token_type
|
|
129
|
+
self.headers = {"Authorization": f"{self._token_type} {token}"}
|
|
130
|
+
if self._on_token is not None:
|
|
131
|
+
self._on_token(token)
|
|
132
|
+
return token
|
|
133
|
+
|
|
134
|
+
def _get(self, url, token, operation):
|
|
135
|
+
self.current_operation = operation
|
|
136
|
+
self.headers = {"Authorization": f"{self._token_type} {token}"}
|
|
137
|
+
return self.call_request(HTTPMethod.GET, url, request_timeout=self._timeout)
|
|
7
138
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
except:
|
|
22
|
-
raise
|
|
23
|
-
|
|
24
|
-
def __login(self):
|
|
25
|
-
try:
|
|
26
|
-
logging.info('api authentication...')
|
|
27
|
-
res = self.call_request(
|
|
28
|
-
http_method=HTTPMethod.POST,
|
|
29
|
-
request_url=UrlUtil().make_url(self.base_url, ['newkoauth','oauth','token']),
|
|
30
|
-
params={
|
|
31
|
-
'username': self.api_login,
|
|
32
|
-
'password': self.api_password,
|
|
33
|
-
'grant_type': self.api_grant_type
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
res = jsonpickle.decode(res)
|
|
37
|
-
if not('error' in res):
|
|
38
|
-
self.token_type = res['token_type']
|
|
39
|
-
self.access_token = res['access_token']
|
|
40
|
-
self.headers = {
|
|
41
|
-
'Authorization': f'{self.token_type} {self.access_token}'
|
|
42
|
-
}
|
|
43
|
-
else:
|
|
44
|
-
raise Exception(res['error_description'])
|
|
45
|
-
except:
|
|
46
|
-
raise
|
|
139
|
+
def _authorized(self, url, operation):
|
|
140
|
+
attempts = 0
|
|
141
|
+
token = self._ensure_token()
|
|
142
|
+
try:
|
|
143
|
+
raw = self._get(url, token, operation)
|
|
144
|
+
except UnauthorizedException:
|
|
145
|
+
attempts += self.last_http_attempts
|
|
146
|
+
token = self._renew_token(token)
|
|
147
|
+
raw = self._get(url, token, operation)
|
|
148
|
+
attempts += self.last_http_attempts
|
|
149
|
+
return raw, attempts
|
|
150
|
+
attempts += self.last_http_attempts
|
|
151
|
+
return raw, attempts
|
getrak/apis/recebidos.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import jsonpickle
|
|
2
6
|
from http import HTTPMethod
|
|
3
7
|
from fmconsult.utils.url import UrlUtil
|
|
4
8
|
from getrak.api import GetrakApi
|
|
9
|
+
from getrak.errors import GetrakError, InvalidVehicleId, VehicleNotFound
|
|
10
|
+
|
|
11
|
+
VEHICLE_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
12
|
+
|
|
5
13
|
|
|
6
14
|
class Recebidos(GetrakApi):
|
|
7
15
|
|
|
@@ -11,4 +19,27 @@ class Recebidos(GetrakApi):
|
|
|
11
19
|
http_method=HTTPMethod.GET,
|
|
12
20
|
request_url=UrlUtil().make_url(self.base_url, ['v0.1','recebidos', id_veiculo, data_inicio, data_fim])
|
|
13
21
|
)
|
|
14
|
-
return jsonpickle.decode(res)
|
|
22
|
+
return jsonpickle.decode(res)
|
|
23
|
+
|
|
24
|
+
def fetch(self, vehicle_id, start, end):
|
|
25
|
+
"""Busca as posições já autenticando sob demanda. Devolve `(lista, tentativas HTTP)`.
|
|
26
|
+
|
|
27
|
+
Valida o ID aqui. `get` continua aceitando o valor que o chamador passar.
|
|
28
|
+
"""
|
|
29
|
+
if not isinstance(vehicle_id, str) or VEHICLE_ID_RE.fullmatch(vehicle_id) is None:
|
|
30
|
+
raise InvalidVehicleId("ID de veículo Getrak inválido")
|
|
31
|
+
url = UrlUtil().make_url(self.base_url, ["v0.1", "recebidos", vehicle_id, start, end])
|
|
32
|
+
raw, attempts = self._authorized(url, "recebidos")
|
|
33
|
+
try:
|
|
34
|
+
body = json.loads(raw)
|
|
35
|
+
except ValueError:
|
|
36
|
+
raise GetrakError("resposta de recebidos não é JSON") from None
|
|
37
|
+
if isinstance(body, dict):
|
|
38
|
+
message = body.get("error") if isinstance(body.get("error"), str) else ""
|
|
39
|
+
if "não encontrado" in message.casefold():
|
|
40
|
+
raise VehicleNotFound(message)
|
|
41
|
+
if not isinstance(body, list):
|
|
42
|
+
raise GetrakError("resposta de recebidos não é uma lista")
|
|
43
|
+
if not all(isinstance(item, dict) for item in body):
|
|
44
|
+
raise GetrakError("resposta de recebidos contém itens que não são objetos")
|
|
45
|
+
return body, attempts
|
getrak/errors.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class GetrakError(Exception):
|
|
2
|
+
"""Erro da integração com a Getrak."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InvalidVehicleId(GetrakError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VehicleNotFound(GetrakError):
|
|
10
|
+
"""A conta não tem esse veículo. Não há lista de posições para gravar."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationFailed(GetrakError):
|
|
14
|
+
"""A Getrak recusou as credenciais. Vale para as chamadas seguintes desta instância."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: getrak-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Client de comunicaçao c/ API da Getrak
|
|
5
5
|
Author-email: Filipe Coelho <filipe@fmconsult.com.br>
|
|
6
6
|
License: MIT License
|
|
@@ -18,4 +18,6 @@ Classifier: Operating System :: OS Independent
|
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
+
Requires-Dist: fmconsult-utils-python-sdk>=2.2.0
|
|
22
|
+
Requires-Dist: jsonpickle>=3.0
|
|
21
23
|
Dynamic: license-file
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
1
|
getrak/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
getrak/api.py,sha256=
|
|
2
|
+
getrak/api.py,sha256=sRhJdXUknljeuvEsJPO1gTBscoUlm0KD_NwS-9BbhyA,5890
|
|
3
|
+
getrak/errors.py,sha256=mobmS8zzjzL1giAlj8o6OTX21gkz1OVbmd2n3CMNZOc,381
|
|
4
4
|
getrak/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
getrak/apis/localizacoes.py,sha256=UuRqEHRMecUQRdSUVq7gRXLPeKe4IZ0byYFlGbDn-6U,654
|
|
6
|
-
getrak/apis/recebidos.py,sha256=
|
|
6
|
+
getrak/apis/recebidos.py,sha256=pUWFyXiu-JWy0fykEKBETA7DTGM5jYbpwuqXsAQoZz0,1792
|
|
7
7
|
getrak/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
getrak/database/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
getrak/database/models/dataexplained.py,sha256=9qjXvccphYUVK0LTNvxjMXRzMOsynbMmcYDkaK41Tdo,212
|
|
10
10
|
getrak/database/models/vehicle.py,sha256=04SVD0TVwnF-4bLbMmdN0dKd9KboMa1-KfxkJ8Fwpv8,2941
|
|
11
|
-
getrak_python_sdk-1.
|
|
12
|
-
getrak_python_sdk-1.
|
|
13
|
-
getrak_python_sdk-1.
|
|
14
|
-
getrak_python_sdk-1.
|
|
15
|
-
getrak_python_sdk-1.
|
|
11
|
+
getrak_python_sdk-1.2.0.dist-info/licenses/LICENSE,sha256=YZjQnFjzXGFa0D3iCQkNDwGBNuTGJREkRmItGX1B3YM,122
|
|
12
|
+
getrak_python_sdk-1.2.0.dist-info/METADATA,sha256=r_VOClVT49asa7dbWvWf0-6qDpwFfA4mT6D2PSR8Ku4,849
|
|
13
|
+
getrak_python_sdk-1.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
getrak_python_sdk-1.2.0.dist-info/top_level.txt,sha256=FTZAzVY_kDvwVRXKSWipeV7H-1ztlyuOMVHhyCEkQO0,7
|
|
15
|
+
getrak_python_sdk-1.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
getrak
|
__init__.py
DELETED
|
File without changes
|
|
File without changes
|