Skip to content

Commit f2cda7c

Browse files
committed
PR #435: refactor get_*_requests()
1 parent 3f11470 commit f2cda7c

File tree

2 files changed

+138
-71
lines changed

2 files changed

+138
-71
lines changed

openfoodfacts/api.py

Lines changed: 101 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import warnings
44
from pathlib import Path
5-
from typing import Any, Dict, List, Optional, Tuple, Union, cast
5+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
66

77
import requests
88

@@ -16,6 +16,59 @@ def get_http_auth(environment: Environment) -> Optional[Tuple[str, str]]:
1616
return ("off", "off") if environment is Environment.net else None
1717

1818

19+
def send_request(
20+
url: str,
21+
api_config: APIConfig,
22+
method: Literal["get", "post", "delete", "head", "put"] = "get",
23+
return_none_on_404: bool = False,
24+
**request_args,
25+
) -> requests.Response | None:
26+
"""Send an HTTP request to the given URL.
27+
28+
:param url: the URL to send the request to.
29+
:param api_config: the API configuration.
30+
:param method: type of HTTP request. Defaults to "get".
31+
:param return_none_on_404: if True, None is returned if the response
32+
status code is 404, defaults to False.
33+
:param request_args: Optional arguments that requests.Session.request takes
34+
:return: the API response.
35+
"""
36+
# Raise some errors early on unknown methods
37+
if method.lower() not in ("get", "post", "delete", "head", "put"):
38+
raise NotImplementedError(f'The HTTP method "{method}" is not implemented.')
39+
http_session_call = getattr(http_session, method.lower())
40+
41+
# Build up the request
42+
request: JSONType = request_args | {
43+
"url": url,
44+
"headers": request_args.get("headers", dict())
45+
| {"User-Agent": api_config.user_agent},
46+
"timeout": api_config.timeout,
47+
}
48+
49+
# Handle authentication
50+
if api_config.username and api_config.password:
51+
key = "json" if request_args.get("json") else "data"
52+
request[key] = request_args.get(key, dict()) | {
53+
"user_id": api_config.username,
54+
"password": api_config.password,
55+
}
56+
elif api_config.session_cookie:
57+
request["cookies"] = request_args.get("cookies", dict()) | {
58+
"session": api_config.session_cookie,
59+
}
60+
if "auth" not in request:
61+
request["auth"] = get_http_auth(api_config.environment)
62+
63+
# Handle request and return data
64+
r = http_session_call(**request)
65+
if r.status_code == 404 and return_none_on_404:
66+
return None
67+
r.raise_for_status()
68+
return r
69+
70+
71+
@warnings.deprecated("Use send_request() instead.")
1972
def send_get_request(
2073
url: str,
2174
api_config: APIConfig,
@@ -32,62 +85,42 @@ def send_get_request(
3285
status code is 404, defaults to False
3386
:return: the API response
3487
"""
35-
r = http_session.get(
88+
r = send_request(
3689
url,
90+
api_config=api_config,
3791
params=params,
38-
headers={"User-Agent": api_config.user_agent},
39-
timeout=api_config.timeout,
92+
return_none_on_404=return_none_on_404,
4093
auth=auth,
94+
method="get",
4195
)
42-
if r.status_code == 404 and return_none_on_404:
96+
if r is None:
4397
return None
44-
r.raise_for_status()
45-
return r.json()
98+
else:
99+
return r.json()
46100

47101

102+
@warnings.deprecated("Use send_request() instead.")
48103
def send_form_urlencoded_post_request(
49104
url: str, body: Dict[str, Any], api_config: APIConfig
50105
) -> requests.Response:
51-
cookies = None
52-
if api_config.username and api_config.password:
53-
body["user_id"] = api_config.username
54-
body["password"] = api_config.password
55-
elif api_config.session_cookie:
56-
cookies = {
57-
"session": api_config.session_cookie,
58-
}
59-
r = http_session.post(
106+
return send_request(
60107
url,
61108
data=body,
62-
headers={"User-Agent": api_config.user_agent},
63-
timeout=api_config.timeout,
64-
auth=get_http_auth(api_config.environment),
65-
cookies=cookies,
109+
api_config=api_config,
110+
method="post",
66111
)
67-
r.raise_for_status()
68-
return r
69112

70113

114+
@warnings.deprecated("Use send_request() instead.")
71115
def send_json_post_request(
72116
url: str, body: JSONType, api_config: APIConfig
73117
) -> requests.Response:
74-
cookies = None
75-
if api_config.username and api_config.password:
76-
body["user_id"] = api_config.username
77-
body["password"] = api_config.password
78-
elif api_config.session_cookie:
79-
cookies = {
80-
"session": api_config.session_cookie,
81-
}
82-
r = http_session.post(
118+
return send_request(
83119
url,
84120
json=body,
85-
headers={"User-Agent": api_config.user_agent},
86-
timeout=api_config.timeout,
87-
auth=get_http_auth(api_config.environment),
88-
cookies=cookies,
121+
api_config=api_config,
122+
method="post",
89123
)
90-
return r
91124

92125

93126
class RobotoffResource:
@@ -136,14 +169,12 @@ def get(
136169
"""
137170
facet = Facet.from_str_or_enum(facet_name)
138171
facet_plural = facet.value.replace("_", "-")
139-
resp = send_get_request(
172+
return send_request(
140173
url=f"{self.base_url}/facets/{facet_plural}",
141174
params={"json": "1", "page": page, "page_size": page_size, **kwargs},
142175
api_config=self.api_config,
143-
auth=get_http_auth(self.api_config.environment),
144-
)
145-
resp = cast(JSONType, resp)
146-
return resp
176+
method="get",
177+
).json()
147178

148179
def get_products(
149180
self,
@@ -175,14 +206,12 @@ def get_products(
175206
if sort_by is not None:
176207
params["sort_by"] = sort_by
177208

178-
resp = send_get_request(
209+
return send_request(
179210
url=f"{self.base_url}/facets/{facet_plural}/{facet_value}",
180211
params=params,
181212
api_config=self.api_config,
182-
auth=get_http_auth(self.api_config.environment),
183-
)
184-
resp = cast(JSONType, resp)
185-
return resp
213+
method="get",
214+
).json()
186215

187216

188217
class ProductResource:
@@ -224,13 +253,18 @@ def get(
224253
# https://github.com/openfoodfacts/openfoodfacts-server/issues/1607
225254
url += "?fields={}".format(",".join(fields))
226255

227-
resp = send_get_request(
228-
url=url, api_config=self.api_config, return_none_on_404=True
256+
resp = send_request(
257+
url=url,
258+
api_config=self.api_config,
259+
return_none_on_404=True,
260+
method="get",
229261
)
230262

231263
if resp is None:
232264
# product not found
233265
return None
266+
else:
267+
resp = resp.json()
234268

235269
if resp["status"] == 0:
236270
# invalid barcode
@@ -267,20 +301,25 @@ def text_search(
267301
if sort_by is not None:
268302
params["sort_by"] = sort_by
269303

270-
return send_get_request(
304+
return send_request(
271305
url=f"{self.base_url}/cgi/search.pl",
272306
api_config=self.api_config,
273307
params=params,
274-
auth=get_http_auth(self.api_config.environment),
275-
)
308+
method="get",
309+
).json()
276310

277311
def update(self, body: Dict[str, Any]):
278312
"""Create a new product or update an existing one."""
279313
if not body.get("code"):
280314
raise ValueError("missing code from body")
281315

282316
url = f"{self.base_url}/cgi/product_jqm2.pl"
283-
return send_form_urlencoded_post_request(url, body, self.api_config)
317+
return send_request(
318+
url,
319+
api_config=self.api_config,
320+
method="post",
321+
data=body,
322+
)
284323

285324
def select_image(
286325
self,
@@ -333,32 +372,18 @@ def select_image(
333372
if image_key is not None:
334373
params["id"] = image_key
335374

336-
cookies = None
337-
if self.api_config.session_cookie:
338-
cookies = {
339-
"session": self.api_config.session_cookie,
340-
}
341-
elif self.api_config.username:
342-
params["user_id"] = self.api_config.username
343-
params["password"] = self.api_config.password
344-
345-
if cookies is None and not params.get("password"):
375+
if not (self.api_config.session_cookie or self.api_config.password):
346376
raise ValueError(
347377
"a password or a session cookie is required to select an image"
348378
)
349379

350-
r = http_session.post(
380+
return send_request(
351381
url,
352382
data=params,
353-
headers={"User-Agent": self.api_config.user_agent},
354-
timeout=self.api_config.timeout,
355-
auth=get_http_auth(self.api_config.environment),
356-
cookies=cookies,
383+
api_config=self.api_config,
384+
method="post",
357385
)
358386

359-
r.raise_for_status()
360-
return r
361-
362387
def parse_ingredients(
363388
self, text: str, lang: str, timeout: int = 10
364389
) -> list[JSONType]:
@@ -549,7 +574,12 @@ def upload_image(
549574
if selected:
550575
data["selected"] = selected
551576

552-
return send_json_post_request(url, data, api_config)
577+
return send_request(
578+
url,
579+
json=data,
580+
api_config=api_config,
581+
method="post",
582+
)
553583

554584

555585
class API:

tests/unit/test_api.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,40 @@ def test_upload_image_with_path(self, tmp_path):
397397
"user_id": "test",
398398
"password": "test",
399399
}
400+
401+
402+
class TestSendRequest:
403+
"""Unit tests for the api.send_request function."""
404+
405+
api_config = openfoodfacts.API(user_agent=TEST_USER_AGENT, version="v2").api_config
406+
407+
def test_invalid_method(self):
408+
method = "invalid"
409+
with pytest.raises(NotImplementedError) as excinfo:
410+
openfoodfacts.api.send_request(
411+
"https://example.com/",
412+
api_config=self.api_config,
413+
method=method,
414+
)
415+
assert method in str(excinfo.value)
416+
417+
def test_get_404_returns_none(self):
418+
method = "get"
419+
url = "https://world.openfoodfacts.org/api/v2/product/1223435"
420+
response_data = {
421+
"status": 0,
422+
"status_verbose": "product not found",
423+
}
424+
with requests_mock.mock() as mock:
425+
mock.get(
426+
url,
427+
text=json.dumps(response_data),
428+
status_code=404,
429+
)
430+
res = openfoodfacts.api.send_request(
431+
url,
432+
api_config=self.api_config,
433+
method=method,
434+
return_none_on_404=True,
435+
)
436+
assert res is None

0 commit comments

Comments
 (0)