Skip to content

Commit 0289d1c

Browse files
feat: update APIClient class
1 parent 92c34c0 commit 0289d1c

6 files changed

Lines changed: 130 additions & 85 deletions

File tree

api-batch-test/src/api_client.py

Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,123 @@
1+
import time
2+
import pytest
13
import requests
4+
from requests.adapters import HTTPAdapter
5+
from urllib3.util.retry import Retry
6+
from config import settings
7+
28

39
class APIClient:
4-
def __init__(self, base_url):
5-
self.base_url = base_url
10+
"""Cliente API com suporte a retry, rate-limit e formatação de URLs."""
11+
12+
def __init__(self, base_url: str, connect_timeout: float, read_timeout: float, retry_total: int, rate_sleep: float):
13+
"""
14+
Inicializa o cliente API.
15+
16+
Args:
17+
base_url: URL base da API
18+
connect_timeout: Timeout de conexão em segundos
19+
read_timeout: Timeout de leitura em segundos
20+
retry_total: Total de tentativas para requisições
21+
rate_sleep: Tempo de espera entre requisições (rate-limit)
22+
"""
23+
self.base_url = base_url.rstrip("/")
24+
self.session = requests.Session()
25+
retry = Retry(
26+
total=retry_total,
27+
backoff_factor=0.5,
28+
status_forcelist=[429, 500, 502, 503, 504],
29+
allowed_methods=["GET", "POST", "PUT", "DELETE"],
30+
respect_retry_after_header=True,
31+
)
32+
adapter = HTTPAdapter(
33+
max_retries=retry, pool_connections=10, pool_maxsize=10)
34+
self.session.mount("https://", adapter)
35+
self.session.mount("http://", adapter)
36+
self.session.headers.update({"Accept": "application/json"})
37+
self.timeout = (connect_timeout, read_timeout)
38+
self.rate_sleep = rate_sleep
39+
40+
def _build_url(self, endpoint: str, **path_params) -> str:
41+
"""Constrói a URL completa formatando parametros de caminho."""
42+
if path_params:
43+
endpoint = endpoint.format(**path_params)
44+
return f"{self.base_url}/{endpoint.lstrip('/')}"
45+
46+
def get(self, endpoint: str, params=None, **kwargs) -> requests.Response:
47+
"""
48+
Faz uma requisição GET.
49+
50+
Args:
51+
endpoint: Endpoint da API (pode ter placeholders como {id})
52+
params: Parâmetros de query
53+
**kwargs: Parâmetros de caminho como id=123
54+
55+
Returns:
56+
Response object
57+
"""
58+
url = self._build_url(endpoint, **kwargs)
59+
time.sleep(self.rate_sleep)
60+
return self.session.get(url, params=params, timeout=self.timeout)
61+
62+
def post(self, endpoint: str, data=None, **kwargs) -> requests.Response:
63+
"""
64+
Faz uma requisição POST.
65+
66+
Args:
67+
endpoint: Endpoint da API
68+
data: Dados a enviar no body (será convertido para JSON)
69+
**kwargs: Parâmetros de caminho
70+
71+
Returns:
72+
Response object
73+
"""
74+
url = self._build_url(endpoint, **kwargs)
75+
time.sleep(self.rate_sleep)
76+
return self.session.post(url, json=data, timeout=self.timeout)
77+
78+
def put(self, endpoint: str, data=None, **kwargs) -> requests.Response:
79+
"""
80+
Faz uma requisição PUT.
81+
82+
Args:
83+
endpoint: Endpoint da API
84+
data: Dados a enviar no body (será convertido para JSON)
85+
**kwargs: Parâmetros de caminho
86+
87+
Returns:
88+
Response object
89+
"""
90+
url = self._build_url(endpoint, **kwargs)
91+
time.sleep(self.rate_sleep)
92+
return self.session.put(url, json=data, timeout=self.timeout)
693

7-
def get(self, endpoint, params=None, **kwargs):
94+
def delete(self, endpoint: str, **kwargs) -> requests.Response:
895
"""
9-
Makes a GET request.
10-
96+
Faz uma requisição DELETE.
97+
1198
Args:
12-
endpoint: API endpoint (can have {id} placeholders)
13-
params: Query parameters
14-
**kwargs: Path parameters like id=123
15-
"""
16-
url = endpoint.format(**kwargs) if kwargs else endpoint
17-
response = requests.get(f"{self.base_url}{url}", params=params)
18-
return response
19-
20-
def post(self, endpoint, data=None):
21-
response = requests.post(f"{self.base_url}{endpoint}", json=data)
22-
return response
23-
24-
def put(self, endpoint, data=None, **kwargs):
25-
url = endpoint.format(**kwargs) if kwargs else endpoint
26-
response = requests.put(f"{self.base_url}{url}", json=data)
27-
return response
28-
29-
def delete(self, endpoint, **kwargs):
30-
url = endpoint.format(**kwargs) if kwargs else endpoint
31-
response = requests.delete(f"{self.base_url}{url}")
32-
return response
99+
endpoint: Endpoint da API
100+
**kwargs: Parâmetros de caminho
101+
102+
Returns:
103+
Response object
104+
"""
105+
url = self._build_url(endpoint, **kwargs)
106+
time.sleep(self.rate_sleep)
107+
return self.session.delete(url, timeout=self.timeout)
108+
109+
110+
@pytest.fixture(scope="session")
111+
def base_url() -> str:
112+
return settings.BASE_URL
113+
114+
115+
@pytest.fixture(scope="session")
116+
def http() -> APIClient:
117+
return APIClient(
118+
base_url=settings.BASE_URL,
119+
connect_timeout=settings.CONNECT_TIMEOUT,
120+
read_timeout=settings.READ_TIMEOUT,
121+
retry_total=settings.RETRY_TOTAL,
122+
rate_sleep=settings.RATE_LIMIT_SLEEP,
123+
)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
class Endpoints:
2-
ACTIVITIES = "/api/v1/Activities"
3-
ACTIVITIES_BY_ID = "/api/v1/Activities/{id}"
2+
PEOPLE = "/people/"
3+
PEOPLE_ID = "/people/{id}"

api-batch-test/tests/conftest.py

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,4 @@
1-
import time
2-
import pytest
3-
import requests
4-
from requests.adapters import HTTPAdapter
5-
from urllib3.util.retry import Retry
6-
from config import settings
7-
8-
9-
class HttpClient:
10-
"""Cliente HTTP focado em testes: timeout, retry e rate-limit simples."""
11-
12-
def __init__(self, base_url: str, connect_timeout: float, read_timeout: float, retry_total: int, rate_sleep: float):
13-
self.base_url = base_url.rstrip("/")
14-
self.session = requests.Session()
15-
retry = Retry(
16-
total=retry_total,
17-
backoff_factor=0.5,
18-
status_forcelist=[429, 500, 502, 503, 504],
19-
allowed_methods=["GET"],
20-
respect_retry_after_header=True,
21-
)
22-
adapter = HTTPAdapter(
23-
max_retries=retry, pool_connections=10, pool_maxsize=10)
24-
self.session.mount("https://", adapter)
25-
self.session.mount("http://", adapter)
26-
self.session.headers.update({"Accept": "application/json"})
27-
self.timeout = (connect_timeout, read_timeout)
28-
self.rate_sleep = rate_sleep
29-
30-
def get(self, path_or_url: str, **kwargs) -> requests.Response:
31-
"""GET com base_url opcional e respeito a rate-limit."""
32-
if path_or_url.startswith("http://") or path_or_url.startswith("https://"):
33-
url = path_or_url
34-
else:
35-
url = f"{self.base_url}/{path_or_url.lstrip('/')}"
36-
time.sleep(self.rate_sleep) # rate-limit simples
37-
return self.session.get(url, timeout=self.timeout, **kwargs)
38-
39-
40-
@pytest.fixture(scope="session")
41-
def base_url() -> str:
42-
return settings.BASE_URL
43-
44-
45-
@pytest.fixture(scope="session")
46-
def http() -> HttpClient:
47-
return HttpClient(
48-
base_url=settings.BASE_URL,
49-
connect_timeout=settings.CONNECT_TIMEOUT,
50-
read_timeout=settings.READ_TIMEOUT,
51-
retry_total=settings.RETRY_TOTAL,
52-
rate_sleep=settings.RATE_LIMIT_SLEEP,
53-
)
1+
from src.api_client import base_url, http
542

3+
# Re-exporting fixtures from src.api_client
4+
__all__ = ["base_url", "http"]

api-batch-test/tests/helpers/pagination.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from typing import Dict, Generator
2-
from tests.conftest import HttpClient
2+
from src.api_client import APIClient
33

4-
def iter_pages(http: HttpClient, first_path: str) -> Generator[Dict, None, None]:
4+
5+
def iter_pages(http: APIClient, first_path: str) -> Generator[Dict, None, None]:
56
# Iterate through pages starting from first_path
67
resp = http.get(first_path)
78
resp.raise_for_status()

api-batch-test/tests/test_people_contract.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
from tests.schemas.page_schema import PAGE_SCHEMA
33
from tests.schemas.people_schema import PEOPLE_ITEM_SCHEMA
44
import pytest
5+
from src.api_endpoints import Endpoints
6+
57

68
@pytest.mark.contract
79
def test_people_page_schema(http):
8-
r = http.get("/people/")
10+
r = http.get(Endpoints.PEOPLE)
911
r.raise_for_status()
1012
data = r.json()
1113
validate(instance=data, schema=PAGE_SCHEMA)

api-batch-test/tests/test_smoke.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pytest
2+
from src.api_endpoints import Endpoints
23

34
@pytest.mark.smoke
45
def test_api_available(http, base_url):
5-
r = http.get("/people/")
6+
r = http.get(Endpoints.PEOPLE)
67
assert r.status_code == 200
78
assert r.headers.get("Content-Type", "").startswith("application/json")

0 commit comments

Comments
 (0)