Skip to content

Commit e04d98e

Browse files
authored
Cache calls to external API services for one week (mozilla#4020)
1 parent 1dbdaf4 commit e04d98e

6 files changed

Lines changed: 347 additions & 96 deletions

File tree

pontoon/machinery/openai_service.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,36 @@
22

33
from openai import OpenAI
44

5+
from django.conf import settings
6+
from django.core.cache import cache
7+
58
from pontoon.base.models import Locale
6-
from pontoon.settings.base import OPENAI_API_KEY
9+
from pontoon.machinery.utils import (
10+
get_machinery_service_cache_key,
11+
set_machinery_service_cache_key,
12+
)
713

814

915
class OpenAIService:
1016
def __init__(self):
11-
if not OPENAI_API_KEY:
17+
if not settings.OPENAI_API_KEY:
1218
raise ValueError("Missing OpenAI API key")
1319
self.client = OpenAI()
1420

1521
def get_translation(
1622
self, english_text, translated_text, characteristic, target_language_name
1723
):
24+
cache_key = get_machinery_service_cache_key(
25+
"openai_chatgpt",
26+
english_text,
27+
translated_text,
28+
characteristic,
29+
target_language_name,
30+
)
31+
cached = cache.get(cache_key)
32+
if cached is not None:
33+
return cached
34+
1835
try:
1936
target_language = Locale.objects.get(name=target_language_name)
2037
except Locale.DoesNotExist:
@@ -94,4 +111,6 @@ def get_translation(
94111
top_p=1, # Set top_p to 1 to consider the full distribution
95112
)
96113

97-
return response.choices[0].message.content.strip()
114+
result = response.choices[0].message.content.strip()
115+
set_machinery_service_cache_key(cache_key, result)
116+
return result

pontoon/machinery/tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ def google_translate_api_key(settings):
1515
key = "2fffff"
1616
settings.GOOGLE_TRANSLATE_API_KEY = key
1717
return key
18+
19+
20+
@pytest.fixture
21+
def openai_api_key(settings):
22+
"""Set the settings.OPENAI_API_KEY for this test"""
23+
key = "3fffff"
24+
settings.OPENAI_API_KEY = key
25+
return key

pontoon/machinery/tests/test_views.py

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import json
22
import urllib.parse
33

4+
from unittest.mock import MagicMock, patch
5+
46
import pytest
7+
import requests
58
import requests_mock
69

10+
from django.core.cache import cache
711
from django.urls import reverse
812

913
from pontoon.base.models import (
@@ -62,6 +66,62 @@ def test_view_microsoft_translator_bad_locale(member, ms_locale, ms_api_key):
6266
assert response.status_code == 401
6367

6468

69+
@pytest.mark.django_db
70+
def test_view_microsoft_translator_missing_api_key(member, ms_locale, settings):
71+
settings.MICROSOFT_TRANSLATOR_API_KEY = ""
72+
cache.clear()
73+
url = reverse("pontoon.microsoft_translator")
74+
response = member.client.get(
75+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
76+
)
77+
assert response.status_code == 400
78+
79+
80+
@pytest.mark.django_db
81+
def test_view_microsoft_translator_api_http_error(member, ms_locale, ms_api_key):
82+
cache.clear()
83+
url = reverse("pontoon.microsoft_translator")
84+
with requests_mock.mock() as m:
85+
m.post(
86+
"https://api.cognitive.microsofttranslator.com/translate",
87+
status_code=401,
88+
)
89+
response = member.client.get(
90+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
91+
)
92+
assert response.status_code == 401
93+
94+
95+
@pytest.mark.django_db
96+
def test_view_microsoft_translator_api_connection_error(member, ms_locale, ms_api_key):
97+
cache.clear()
98+
url = reverse("pontoon.microsoft_translator")
99+
with requests_mock.mock() as m:
100+
m.post(
101+
"https://api.cognitive.microsofttranslator.com/translate",
102+
exc=requests.exceptions.ConnectionError,
103+
)
104+
response = member.client.get(
105+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
106+
)
107+
assert response.status_code == 500
108+
109+
110+
@pytest.mark.django_db
111+
def test_view_microsoft_translator_api_error_in_response(member, ms_locale, ms_api_key):
112+
cache.clear()
113+
url = reverse("pontoon.microsoft_translator")
114+
with requests_mock.mock() as m:
115+
m.post(
116+
"https://api.cognitive.microsofttranslator.com/translate",
117+
json={"error": {"code": 400000, "message": "Bad request"}},
118+
)
119+
response = member.client.get(
120+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
121+
)
122+
assert response.status_code == 400
123+
124+
65125
@pytest.mark.django_db
66126
def test_view_google_translate_not_logged_in(
67127
client, google_translate_locale, google_translate_api_key
@@ -90,7 +150,6 @@ def test_view_google_translate(
90150

91151
assert response.status_code == 200
92152
assert json.loads(response.content) == {
93-
"status": True,
94153
"translation": "target",
95154
}
96155

@@ -117,6 +176,146 @@ def test_view_google_translate_bad_locale(
117176
assert response.status_code == 400
118177

119178

179+
@pytest.mark.django_db
180+
def test_view_google_translate_missing_api_key(
181+
member, google_translate_locale, settings
182+
):
183+
settings.GOOGLE_TRANSLATE_API_KEY = ""
184+
cache.clear()
185+
url = reverse("pontoon.google_translate")
186+
response = member.client.get(
187+
url, {"text": "text", "locale": google_translate_locale.code}
188+
)
189+
assert response.status_code == 400
190+
191+
192+
@pytest.mark.django_db
193+
def test_view_google_translate_api_http_error(
194+
member, google_translate_locale, google_translate_api_key
195+
):
196+
cache.clear()
197+
url = reverse("pontoon.google_translate")
198+
with requests_mock.mock() as m:
199+
m.post(
200+
"https://translation.googleapis.com/language/translate/v2",
201+
status_code=403,
202+
)
203+
response = member.client.get(
204+
url, {"text": "text", "locale": google_translate_locale.code}
205+
)
206+
assert response.status_code == 403
207+
208+
209+
@pytest.mark.django_db
210+
def test_view_google_translate_api_connection_error(
211+
member, google_translate_locale, google_translate_api_key
212+
):
213+
cache.clear()
214+
url = reverse("pontoon.google_translate")
215+
with requests_mock.mock() as m:
216+
m.post(
217+
"https://translation.googleapis.com/language/translate/v2",
218+
exc=requests.exceptions.ConnectionError,
219+
)
220+
response = member.client.get(
221+
url, {"text": "text", "locale": google_translate_locale.code}
222+
)
223+
assert response.status_code == 500
224+
225+
226+
@pytest.mark.django_db
227+
def test_view_google_translate_api_unexpected_response(
228+
member, google_translate_locale, google_translate_api_key
229+
):
230+
cache.clear()
231+
url = reverse("pontoon.google_translate")
232+
with requests_mock.mock() as m:
233+
m.post(
234+
"https://translation.googleapis.com/language/translate/v2",
235+
json={"unexpected": "response"},
236+
)
237+
response = member.client.get(
238+
url, {"text": "text", "locale": google_translate_locale.code}
239+
)
240+
assert response.status_code == 400
241+
242+
243+
@pytest.mark.django_db
244+
def test_view_microsoft_translator_cache(member, ms_locale, ms_api_key):
245+
url = reverse("pontoon.microsoft_translator")
246+
cache.clear()
247+
248+
with requests_mock.mock() as m:
249+
data = [{"translations": [{"text": "target"}]}]
250+
m.post("https://api.cognitive.microsofttranslator.com/translate", json=data)
251+
252+
response1 = member.client.get(
253+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
254+
)
255+
assert len(m.request_history) == 1
256+
257+
# Second identical request should be served from cache
258+
response2 = member.client.get(
259+
url, {"text": "text", "locale": ms_locale.ms_translator_code}
260+
)
261+
assert len(m.request_history) == 1
262+
263+
assert json.loads(response1.content) == json.loads(response2.content)
264+
265+
266+
@pytest.mark.django_db
267+
def test_view_google_translate_cache(
268+
member, google_translate_locale, google_translate_api_key
269+
):
270+
url = reverse("pontoon.google_translate")
271+
cache.clear()
272+
273+
with requests_mock.mock() as m:
274+
data = {"data": {"translations": [{"translatedText": "target"}]}}
275+
m.post("https://translation.googleapis.com/language/translate/v2", json=data)
276+
277+
response1 = member.client.get(
278+
url, {"text": "text", "locale": google_translate_locale.code}
279+
)
280+
assert len(m.request_history) == 1
281+
282+
# Second identical request should be served from cache
283+
response2 = member.client.get(
284+
url, {"text": "text", "locale": google_translate_locale.code}
285+
)
286+
assert len(m.request_history) == 1
287+
288+
assert json.loads(response1.content) == json.loads(response2.content)
289+
290+
291+
@pytest.mark.django_db
292+
def test_view_gpt_transform_cache(member, locale_a, openai_api_key):
293+
url = reverse("pontoon.gpt_transform")
294+
cache.clear()
295+
296+
mock_response = MagicMock()
297+
mock_response.choices[0].message.content = " formal translation "
298+
299+
with patch("pontoon.machinery.openai_service.OpenAI") as MockOpenAI:
300+
MockOpenAI.return_value.chat.completions.create.return_value = mock_response
301+
302+
params = {
303+
"english_text": "Hello",
304+
"translated_text": "Hola",
305+
"characteristic": "formal",
306+
"locale": locale_a.name,
307+
}
308+
309+
response1 = member.client.get(url, params)
310+
assert MockOpenAI.return_value.chat.completions.create.call_count == 1
311+
312+
# Second identical request should be served from cache
313+
response2 = member.client.get(url, params)
314+
assert MockOpenAI.return_value.chat.completions.create.call_count == 1
315+
316+
assert json.loads(response1.content) == json.loads(response2.content)
317+
318+
120319
@pytest.mark.django_db
121320
def test_view_caighdean(client, entity_a):
122321
gd = Locale.objects.get(code="gd")

0 commit comments

Comments
 (0)