getrak-python-sdk 1.0.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (25) hide show
  1. {getrak_python_sdk-1.0.0/src/getrak_python_sdk.egg-info → getrak_python_sdk-1.2.0}/PKG-INFO +3 -1
  2. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/pyproject.toml +8 -2
  3. getrak_python_sdk-1.2.0/src/getrak/api.py +151 -0
  4. getrak_python_sdk-1.2.0/src/getrak/apis/recebidos.py +45 -0
  5. getrak_python_sdk-1.2.0/src/getrak/errors.py +14 -0
  6. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0/src/getrak_python_sdk.egg-info}/PKG-INFO +3 -1
  7. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/src/getrak_python_sdk.egg-info/SOURCES.txt +4 -2
  8. getrak_python_sdk-1.2.0/src/getrak_python_sdk.egg-info/requires.txt +2 -0
  9. getrak_python_sdk-1.2.0/src/getrak_python_sdk.egg-info/top_level.txt +1 -0
  10. getrak_python_sdk-1.2.0/tests/test_client.py +138 -0
  11. getrak_python_sdk-1.0.0/src/getrak/api.py +0 -46
  12. getrak_python_sdk-1.0.0/src/getrak/apis/recebidos.py +0 -14
  13. getrak_python_sdk-1.0.0/src/getrak/database/models/__init__.py +0 -0
  14. getrak_python_sdk-1.0.0/src/getrak_python_sdk.egg-info/top_level.txt +0 -2
  15. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/LICENSE +0 -0
  16. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/README.md +0 -0
  17. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/setup.cfg +0 -0
  18. {getrak_python_sdk-1.0.0/src → getrak_python_sdk-1.2.0/src/getrak}/__init__.py +0 -0
  19. {getrak_python_sdk-1.0.0/src/getrak → getrak_python_sdk-1.2.0/src/getrak/apis}/__init__.py +0 -0
  20. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/src/getrak/apis/localizacoes.py +0 -0
  21. {getrak_python_sdk-1.0.0/src/getrak/apis → getrak_python_sdk-1.2.0/src/getrak/database}/__init__.py +0 -0
  22. {getrak_python_sdk-1.0.0/src/getrak/database → getrak_python_sdk-1.2.0/src/getrak/database/models}/__init__.py +0 -0
  23. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/src/getrak/database/models/dataexplained.py +0 -0
  24. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/src/getrak/database/models/vehicle.py +0 -0
  25. {getrak_python_sdk-1.0.0 → getrak_python_sdk-1.2.0}/src/getrak_python_sdk.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getrak-python-sdk
3
- Version: 1.0.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "getrak-python-sdk"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "Client de comunicaçao c/ API da Getrak"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -19,7 +19,13 @@ classifiers = [
19
19
  "Operating System :: OS Independent",
20
20
  ]
21
21
 
22
- dependencies = []
22
+ dependencies = [
23
+ "fmconsult-utils-python-sdk>=2.2.0",
24
+ "jsonpickle>=3.0",
25
+ ]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
23
29
 
24
30
  [project.urls]
25
31
  "Homepage" = "https://github.com/seuuser/minha-biblioteca"
@@ -0,0 +1,151 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import threading
5
+ import time
6
+ from http import HTTPMethod
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
12
+ from fmconsult.utils.url import UrlUtil
13
+
14
+ from getrak.errors import AuthenticationFailed, GetrakError
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ AUTH_REJECTED = (BadRequestException, UnauthorizedException, ForbiddenException)
19
+
20
+
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)
138
+
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
@@ -0,0 +1,45 @@
1
+ import json
2
+ import logging
3
+ import re
4
+
5
+ import jsonpickle
6
+ from http import HTTPMethod
7
+ from fmconsult.utils.url import UrlUtil
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
+
13
+
14
+ class Recebidos(GetrakApi):
15
+
16
+ def get(self, id_veiculo, data_inicio, data_fim):
17
+ logging.info(f'get vehicle {id_veiculo} records from {data_inicio} to {data_fim}...')
18
+ res = self.call_request(
19
+ http_method=HTTPMethod.GET,
20
+ request_url=UrlUtil().make_url(self.base_url, ['v0.1','recebidos', id_veiculo, data_inicio, data_fim])
21
+ )
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
@@ -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.0.0
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,9 +1,9 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- src/__init__.py
5
4
  src/getrak/__init__.py
6
5
  src/getrak/api.py
6
+ src/getrak/errors.py
7
7
  src/getrak/apis/__init__.py
8
8
  src/getrak/apis/localizacoes.py
9
9
  src/getrak/apis/recebidos.py
@@ -14,4 +14,6 @@ src/getrak/database/models/vehicle.py
14
14
  src/getrak_python_sdk.egg-info/PKG-INFO
15
15
  src/getrak_python_sdk.egg-info/SOURCES.txt
16
16
  src/getrak_python_sdk.egg-info/dependency_links.txt
17
- src/getrak_python_sdk.egg-info/top_level.txt
17
+ src/getrak_python_sdk.egg-info/requires.txt
18
+ src/getrak_python_sdk.egg-info/top_level.txt
19
+ tests/test_client.py
@@ -0,0 +1,2 @@
1
+ fmconsult-utils-python-sdk>=2.2.0
2
+ jsonpickle>=3.0
@@ -0,0 +1,138 @@
1
+ import base64
2
+ from urllib.parse import parse_qs
3
+
4
+ import pytest
5
+ import responses
6
+ from responses import registries
7
+
8
+ from getrak.apis.recebidos import Recebidos
9
+ from getrak.errors import AuthenticationFailed, GetrakError, InvalidVehicleId, VehicleNotFound
10
+
11
+ BASE = "https://api.getrak.com"
12
+ AUTH = f"{BASE}/newkoauth/oauth/token"
13
+ POS = f"{BASE}/v0.1/recebidos/12345/2026-10-04T03:00:00/2026-10-05T02:59:59"
14
+ PASSWORD = "SenhaSuperSecreta123"
15
+ AUTH_CODE = base64.b64encode(b"cliente:segredo").decode()
16
+
17
+
18
+ @pytest.fixture
19
+ def env(monkeypatch):
20
+ monkeypatch.setenv("getrak.api.auth_code", AUTH_CODE)
21
+ monkeypatch.setenv("getrak.api.login", "usuario")
22
+ monkeypatch.setenv("getrak.api.password", PASSWORD)
23
+ monkeypatch.setenv("getrak.api.grant_type", "password")
24
+
25
+
26
+ def test_recebidos_keeps_constructor_and_sends_password_in_the_body(env):
27
+ with responses.RequestsMock(registry=registries.OrderedRegistry) as http:
28
+ http.add(responses.POST, AUTH, json={"access_token": "tok", "token_type": "bearer"}, status=200)
29
+ http.add(responses.GET, POS, json=[{"lat": 1}], status=200)
30
+ records = Recebidos().get("12345", "2026-10-04T03:00:00", "2026-10-05T02:59:59")
31
+ sent = http.calls[0].request
32
+ assert "password" not in sent.url
33
+ assert parse_qs(sent.body)["password"] == [PASSWORD]
34
+ assert sent.headers["Authorization"] == f"Basic {AUTH_CODE}"
35
+ assert http.calls[1].request.headers["Authorization"] == "Bearer tok"
36
+
37
+ assert records == [{"lat": 1}]
38
+
39
+
40
+ def test_password_is_not_logged(env, caplog):
41
+ caplog.set_level("INFO")
42
+ with responses.RequestsMock() as http:
43
+ http.add(
44
+ responses.POST,
45
+ AUTH,
46
+ json={"access_token": "tok-secreto-xyz", "token_type": "bearer"},
47
+ status=200,
48
+ )
49
+ Recebidos()
50
+ assert PASSWORD not in caplog.text
51
+ assert "tok-secreto-xyz" not in caplog.text
52
+
53
+
54
+ def test_fetch_reuses_token_and_rejects_bad_id():
55
+ sleeps = []
56
+ with responses.RequestsMock(registry=registries.OrderedRegistry) as http:
57
+ http.add(responses.POST, AUTH, json={"access_token": "tok-1", "token_type": "Bearer"}, status=200)
58
+ http.add(responses.GET, POS, json=[{"a": 1}], status=200)
59
+ client = Recebidos(
60
+ auth_code=AUTH_CODE,
61
+ login="usuario",
62
+ password=PASSWORD,
63
+ grant_type="password",
64
+ authenticate_on_init=False,
65
+ verbose=False,
66
+ log_requests=False,
67
+ retry_transient=True,
68
+ sleep=sleeps.append,
69
+ )
70
+ with pytest.raises(InvalidVehicleId):
71
+ client.fetch("12/34", "2026-10-04T03:00:00", "2026-10-05T02:59:59")
72
+ assert len(http.calls) == 0
73
+ positions, attempts = client.fetch("12345", "2026-10-04T03:00:00", "2026-10-05T02:59:59")
74
+ posts = [call for call in http.calls if call.request.method == "POST"]
75
+
76
+ assert positions == [{"a": 1}]
77
+ assert attempts == 1
78
+ assert len(posts) == 1
79
+
80
+
81
+ def test_rejected_credentials_are_not_retried():
82
+ with responses.RequestsMock() as http:
83
+ http.add(responses.POST, AUTH, status=401)
84
+ client = Recebidos(
85
+ auth_code=AUTH_CODE,
86
+ login="usuario",
87
+ password=PASSWORD,
88
+ grant_type="password",
89
+ authenticate_on_init=False,
90
+ verbose=False,
91
+ log_requests=False,
92
+ retry_transient=True,
93
+ )
94
+ with pytest.raises(AuthenticationFailed):
95
+ client.fetch("12345", "inicio", "fim")
96
+ with pytest.raises(AuthenticationFailed):
97
+ client.fetch("12345", "inicio", "fim")
98
+ assert len(http.calls) == 1
99
+ assert PASSWORD not in str(http.calls[0].request.url)
100
+
101
+
102
+ def test_non_list_is_an_error():
103
+ with responses.RequestsMock() as http:
104
+ http.add(responses.POST, AUTH, json={"access_token": "tok"}, status=200)
105
+ http.add(responses.GET, POS, json={"erro": "x"}, status=200)
106
+ client = Recebidos(
107
+ auth_code=AUTH_CODE,
108
+ login="usuario",
109
+ password=PASSWORD,
110
+ grant_type="password",
111
+ authenticate_on_init=False,
112
+ verbose=False,
113
+ log_requests=False,
114
+ )
115
+ with pytest.raises(GetrakError):
116
+ client.fetch("12345", "2026-10-04T03:00:00", "2026-10-05T02:59:59")
117
+
118
+
119
+ def test_unknown_vehicle_is_a_specific_error():
120
+ with responses.RequestsMock() as http:
121
+ http.add(responses.POST, AUTH, json={"access_token": "tok"}, status=200)
122
+ http.add(
123
+ responses.GET,
124
+ POS,
125
+ json={"error": "Veículo não encontrado para o cliente informado", "status": 404},
126
+ status=200,
127
+ )
128
+ client = Recebidos(
129
+ auth_code=AUTH_CODE,
130
+ login="usuario",
131
+ password=PASSWORD,
132
+ grant_type="password",
133
+ authenticate_on_init=False,
134
+ verbose=False,
135
+ log_requests=False,
136
+ )
137
+ with pytest.raises(VehicleNotFound):
138
+ client.fetch("12345", "2026-10-04T03:00:00", "2026-10-05T02:59:59")
@@ -1,46 +0,0 @@
1
- import logging, os, jsonpickle
2
- from http import HTTPMethod
3
- from fmconsult.http.api import ApiBase
4
- from fmconsult.utils.url import UrlUtil
5
-
6
- class GetrakApi(ApiBase):
7
-
8
- def __init__(self):
9
- try:
10
- self.api_auth_code = os.environ['getrak.api.auth_code']
11
- self.api_login = os.environ['getrak.api.login']
12
- self.api_password = os.environ['getrak.api.password']
13
- self.api_grant_type = os.environ['getrak.api.grant_type']
14
-
15
- self.base_url = 'https://api.getrak.com'
16
- self.headers = {
17
- 'Authorization': f'Basic {self.api_auth_code}'
18
- }
19
-
20
- self.__login()
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
@@ -1,14 +0,0 @@
1
- import logging, jsonpickle
2
- from http import HTTPMethod
3
- from fmconsult.utils.url import UrlUtil
4
- from getrak.api import GetrakApi
5
-
6
- class Recebidos(GetrakApi):
7
-
8
- def get(self, id_veiculo, data_inicio, data_fim):
9
- logging.info(f'get vehicle {id_veiculo} records from {data_inicio} to {data_fim}...')
10
- res = self.call_request(
11
- http_method=HTTPMethod.GET,
12
- request_url=UrlUtil().make_url(self.base_url, ['v0.1','recebidos', id_veiculo, data_inicio, data_fim])
13
- )
14
- return jsonpickle.decode(res)
@@ -1,2 +0,0 @@
1
- __init__
2
- getrak