Skip to content

Commit eeb18f0

Browse files
committed
Use the openfoodfacts skd to download the product images
1 parent 14e77d4 commit eeb18f0

File tree

2 files changed

+108
-108
lines changed

2 files changed

+108
-108
lines changed

wger/nutrition/sync.py

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@
2727

2828
# Third Party
2929
import requests
30-
from openfoodfacts.images import (
31-
AWS_S3_BASE_URL,
32-
generate_image_path,
30+
from openfoodfacts import (
31+
API,
32+
APIVersion,
33+
Country,
34+
Environment,
35+
Flavor,
3336
)
37+
from openfoodfacts.images import download_image
3438
from tqdm import tqdm
3539

3640
# wger
@@ -55,6 +59,7 @@
5559
from wger.utils.requests import (
5660
get_paginated,
5761
wger_headers,
62+
wger_user_agent,
5863
)
5964
from wger.utils.url import make_uri
6065

@@ -135,55 +140,47 @@ def fetch_image_from_off(ingredient: Ingredient):
135140
- https://openfoodfacts.github.io/openfoodfacts-server/api/how-to-download-images/
136141
- https://openfoodfacts.github.io/openfoodfacts-server/api/ref-v2/
137142
"""
138-
logger.info(f'Trying to fetch image from OFF for {ingredient.name} (UUID: {ingredient.uuid})')
139-
140-
url = ingredient.source_url + '?fields=images,image_front_url'
141-
headers = wger_headers()
142-
try:
143-
product_data = requests.get(url, headers=headers, timeout=3).json()
144-
except requests.JSONDecodeError:
145-
logger.warning(f'Could not decode JSON response from {url}')
146-
return
147-
except requests.ConnectTimeout as e:
148-
logger.warning(f'Connection timeout while trying to fetch {url}: {e}')
149-
return
150-
except requests.ReadTimeout as e:
151-
logger.warning(f'Read timeout while trying to fetch {url}: {e}')
152-
return
143+
logger.info(f'Trying to fetch image from OFF for "{ingredient.name}" ({ingredient.uuid})')
144+
off_api = API(
145+
user_agent=wger_user_agent(),
146+
country=Country.world,
147+
flavor=Flavor.off,
148+
version=APIVersion.v2,
149+
environment=Environment.org,
150+
)
151+
product_data = off_api.product.get(ingredient.code, fields=['images', 'image_front_url'])
153152

154-
try:
155-
image_url: Optional[str] = product_data['product'].get('image_front_url')
156-
except KeyError:
157-
logger.info('No "product" key found, exiting...')
153+
if product_data is None:
154+
logger.info('No product data found for this ingredient')
158155
return
159156

157+
image_url: Optional[str] = product_data.get('image_front_url')
160158
if not image_url:
161159
logger.info('Product data has no "image_front_url" key')
162160
return
163-
image_data = product_data['product']['images']
161+
image_data = product_data['images']
162+
if not image_data:
163+
logger.info('Product data has no "images" key')
164+
return
164165

165166
# Extract the image key from the url:
166167
# https://images.openfoodfacts.org/images/products/00975957/front_en.5.400.jpg -> "front_en"
167-
image_id: str = image_url.rpartition('/')[2].partition('.')[0]
168+
image_key: str = image_url.rpartition('/')[2].partition('.')[0]
168169

169-
# Extract the uploader name
170+
# Extract the uploader name and numerical image id
170171
try:
171-
image_id: str = image_data[image_id]['imgid']
172+
image_id: str = image_data[image_key]['imgid']
172173
uploader_name: str = image_data[image_id]['uploader']
173174
except KeyError as e:
174175
logger.info('could not load all image information, skipping...', e)
175176
return
176177

177-
# Download image from amazon
178-
image_s3_url = f'{AWS_S3_BASE_URL}{generate_image_path(ingredient.code, image_id)}'
179-
response = requests.get(image_s3_url, headers=headers)
180-
if not response.ok:
181-
logger.info(f'Could not locate image on AWS! Status code: {response.status_code}')
182-
return
178+
off_response = download_image(image_url, return_struct=True)
183179

184180
# Save to DB
185-
url = (
186-
f'https://world.openfoodfacts.org/cgi/product_image.pl?code={ingredient.code}&id={image_id}'
181+
url = make_uri(
182+
'https://world.openfoodfacts.org/cgi/product_image.pl',
183+
query={'code': ingredient.code, 'id': image_id},
187184
)
188185
uploader_url = f'https://world.openfoodfacts.org/photographer/{uploader_name}'
189186
image_data = {
@@ -194,15 +191,15 @@ def fetch_image_from_off(ingredient: Ingredient):
194191
'license_author_url': uploader_url,
195192
'license_object_url': url,
196193
'license_derivative_source_url': '',
197-
'size': len(response.content),
194+
'size': len(off_response.image_bytes),
198195
}
199196
try:
200-
Image.from_json(ingredient, response, image_data, generate_uuid=True)
197+
Image.from_json(ingredient, off_response.response, image_data, generate_uuid=True)
201198
# Due to a race condition (e.g. when adding tasks over the search), we might
202199
# try to save an image to an ingredient that already has one. In that case,
203200
# just ignore the error
204201
except IntegrityError:
205-
logger.info('Ingredient has already an image, skipping...')
202+
logger.debug('Ingredient has already an image, skipping...')
206203
return
207204
ingredient.last_image_check = timezone.now()
208205
ingredient.save()

wger/nutrition/tests/test_tasks.py

Lines changed: 74 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515

1616
# Standard Library
1717
import datetime
18-
from unittest.mock import (
19-
ANY,
20-
patch,
21-
)
18+
from unittest.mock import patch
2219

2320
# Django
2421
from django.utils import timezone
@@ -37,74 +34,62 @@
3734
from wger.utils.requests import wger_headers
3835

3936

40-
class MockOffResponse:
41-
def __init__(self):
42-
self.status_code = 200
43-
self.ok = True
44-
self.content = b'2000'
37+
mock_off_response = {
38+
'image_front_url': 'https://images.openfoodfacts.org/images/products/00975957/front_en.5.400.jpg',
39+
'images': {
40+
'front_en': {
41+
'imgid': '12345',
42+
},
43+
'12345': {'uploader': 'Mr Foobar'},
44+
},
45+
}
4546

46-
# yapf: disable
47-
@staticmethod
48-
def json():
49-
return {
50-
"product": {
51-
'image_front_url':
52-
'https://images.openfoodfacts.org/images/products/00975957/front_en.5.400.jpg',
53-
'images': {
54-
'front_en': {
55-
'imgid': '12345',
56-
},
57-
'12345': {
58-
'uploader': 'Mr Foobar'
59-
}
60-
}
61-
},
62-
}
63-
# yapf: disable
47+
48+
class MockOffImageResponse:
49+
image_bytes = [0, 1, 2, 3]
50+
response = type('obj', (object,), {'content': b'mock_content'})()
6451

6552

6653
class MockWgerApiResponse:
6754
def __init__(self):
6855
self.status_code = 200
6956
self.content = b'2000'
7057

71-
# yapf: disable
7258
@staticmethod
7359
def json():
7460
return {
75-
"count": 1,
76-
"next": None,
77-
"previous": None,
78-
"results": [
61+
'count': 1,
62+
'next': None,
63+
'previous': None,
64+
'results': [
7965
{
80-
"id": 1,
81-
"uuid": "188324b5-587f-42d7-9abc-d2ca64c73d45",
82-
"ingredient_id": "12345",
83-
"ingredient_uuid": "e9baa8bd-84fc-4756-8d90-5b9739b06cf8",
84-
"image": "http://localhost:8000/media/ingredients/1/188324b5-587f-42d7-9abc-d2ca64c73d45.jpg",
85-
"created": "2023-03-15T23:20:10.969369+01:00",
86-
"last_update": "2023-03-15T23:20:10.969369+01:00",
87-
"size": 20179,
88-
"width": 400,
89-
"height": 166,
90-
"license": 1,
91-
"license_author": "Tester McTest",
92-
"license_author_url": "https://example.com/editors/mcLovin",
93-
"license_title": "",
94-
"license_object_url": "",
95-
"license_derivative_source_url": ""
66+
'id': 1,
67+
'uuid': '188324b5-587f-42d7-9abc-d2ca64c73d45',
68+
'ingredient_id': '12345',
69+
'ingredient_uuid': 'e9baa8bd-84fc-4756-8d90-5b9739b06cf8',
70+
'image': 'http://localhost:8000/media/ingredients/1/188324b5-587f-42d7-9abc-d2ca64c73d45.jpg',
71+
'created': '2023-03-15T23:20:10.969369+01:00',
72+
'last_update': '2023-03-15T23:20:10.969369+01:00',
73+
'size': 20179,
74+
'width': 400,
75+
'height': 166,
76+
'license': 1,
77+
'license_author': 'Tester McTest',
78+
'license_author_url': 'https://example.com/editors/mcLovin',
79+
'license_title': '',
80+
'license_object_url': '',
81+
'license_derivative_source_url': '',
9682
}
97-
]
83+
],
9884
}
99-
# yapf: enable
10085

10186

10287
class FetchIngredientImageTestCase(WgerTestCase):
10388
"""
10489
Test fetching an ingredient image
10590
"""
10691

107-
@patch('requests.get')
92+
@patch('openfoodfacts.api.ProductResource.get')
10893
@patch.object(logger, 'info')
10994
def test_source(self, mock_logger, mock_request):
11095
"""
@@ -120,7 +105,7 @@ def test_source(self, mock_logger, mock_request):
120105
mock_request.assert_not_called()
121106
self.assertEqual(result, None)
122107

123-
@patch('requests.get')
108+
@patch('openfoodfacts.api.ProductResource.get')
124109
@patch.object(logger, 'info')
125110
def test_download_off_setting(self, mock_logger, mock_request):
126111
"""
@@ -136,7 +121,7 @@ def test_download_off_setting(self, mock_logger, mock_request):
136121
mock_request.assert_not_called()
137122
self.assertEqual(result, None)
138123

139-
@patch('requests.get', return_value=MockOffResponse())
124+
@patch('openfoodfacts.api.ProductResource.get')
140125
@patch('wger.nutrition.models.Image.from_json')
141126
@patch.object(logger, 'info')
142127
def test_last_new_image_date(self, mock_logger, mock_from_json, mock_request):
@@ -160,10 +145,17 @@ def test_last_new_image_date(self, mock_logger, mock_from_json, mock_request):
160145
mock_request.assert_not_called()
161146
self.assertEqual(result, None)
162147

163-
@patch('requests.get', return_value=MockOffResponse())
148+
@patch('wger.nutrition.sync.download_image', return_value=MockOffImageResponse)
149+
@patch('openfoodfacts.api.ProductResource.get', return_value=mock_off_response)
164150
@patch('wger.nutrition.models.Image.from_json')
165151
@patch.object(logger, 'info')
166-
def test_last_old_image_date(self, mock_logger, mock_from_json, mock_request):
152+
def test_last_old_image_date(
153+
self,
154+
mock_logger,
155+
mock_from_json,
156+
mock_request,
157+
mock_download_image,
158+
):
167159
"""
168160
Test that images are fetched if we checked a long time ago
169161
"""
@@ -180,14 +172,27 @@ def test_last_old_image_date(self, mock_logger, mock_from_json, mock_request):
180172
):
181173
result = fetch_ingredient_image(1)
182174
mock_from_json.assert_called()
175+
mock_download_image.assert_called_with(
176+
'https://images.openfoodfacts.org/images/products/00975957/front_en.5.400.jpg',
177+
return_struct=True,
178+
)
183179
mock_logger.assert_any_call('Fetching image for ingredient 1')
184-
mock_request.assert_called()
180+
mock_request.assert_called_with(
181+
'1234567890987654321', fields=['images', 'image_front_url']
182+
)
185183
self.assertEqual(result, None)
186184

187-
@patch('requests.get', return_value=MockOffResponse())
185+
@patch('wger.nutrition.sync.download_image', return_value=MockOffImageResponse)
186+
@patch('openfoodfacts.api.ProductResource.get', return_value=mock_off_response)
188187
@patch('wger.nutrition.models.Image.from_json')
189188
@patch.object(logger, 'info')
190-
def test_download_ingredient_off(self, mock_logger, mock_from_json, mock_request):
189+
def test_download_ingredient_off(
190+
self,
191+
mock_logger,
192+
mock_from_json,
193+
mock_request,
194+
mock_download_image,
195+
):
191196
"""
192197
Test that the image is correctly downloaded
193198
@@ -202,23 +207,21 @@ def test_download_ingredient_off(self, mock_logger, mock_from_json, mock_request
202207
):
203208
result = fetch_ingredient_image(1)
204209

210+
# print(mock_request.mock_calls)
211+
mock_request.assert_called_with(
212+
'1234567890987654321',
213+
fields=['images', 'image_front_url'],
214+
)
215+
mock_download_image.assert_called_with(
216+
'https://images.openfoodfacts.org/images/products/00975957/front_en.5.400.jpg',
217+
return_struct=True,
218+
)
205219
mock_logger.assert_any_call('Fetching image for ingredient 1')
206220
mock_logger.assert_any_call(
207-
'Trying to fetch image from OFF for Test ingredient 1 (UUID: '
208-
'7908c204-907f-4b1e-ad4e-f482e9769ade)'
221+
'Trying to fetch image from OFF for "Test ingredient 1" '
222+
'(7908c204-907f-4b1e-ad4e-f482e9769ade)'
209223
)
210224
mock_logger.assert_any_call('Image successfully saved')
211-
212-
# print(mock_request.mock_calls)
213-
mock_request.assert_any_call(
214-
'https://world.openfoodfacts.org/api/v2/product/5055365635003.json?fields=images,image_front_url',
215-
headers=wger_headers(),
216-
timeout=ANY,
217-
)
218-
mock_request.assert_any_call(
219-
'https://openfoodfacts-images.s3.eu-west-3.amazonaws.com/data/123/456/789/0987654321/12345.jpg',
220-
headers=wger_headers(),
221-
)
222225
mock_from_json.assert_called()
223226

224227
self.assertEqual(result, None)

0 commit comments

Comments
 (0)