зеркало из https://github.com/mozilla/taar.git
Reworked the URL handler to accept a hashed ID instead of a raw ID.
Added docs, tests and code to enable promoted GUIDs to be passed in through the TAAR API
This commit is contained in:
Родитель
bab992b682
Коммит
3183eab590
|
@ -0,0 +1,94 @@
|
|||
API documentation
|
||||
|
||||
# Get addon recommendations
|
||||
|
||||
Allow the Authenticated User to update their details.
|
||||
|
||||
**URL** : `/v1/api/recommendations/<hashed_id>/`
|
||||
|
||||
**Method** : `POST`
|
||||
|
||||
**Auth required** : NO
|
||||
|
||||
**Permissions required** : None
|
||||
|
||||
**Data constraints**
|
||||
|
||||
```json
|
||||
{
|
||||
"options": {"promoted": [
|
||||
["[1 to 30 chars]", Some Number],
|
||||
["[1 to 30 chars]", Some Number],
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the only valid key for the top level JSON is `options`.
|
||||
|
||||
`options` is always a dictionary of optional values.
|
||||
|
||||
To denote no optional data - it is perfectly valid for the JSON data
|
||||
to have no `options` key, or even simpler - not have POST data at all.
|
||||
|
||||
Each item in the promoted addon GUID list is accompanied by an
|
||||
integer weight. Any weight is greater than a TAAR recommended addon
|
||||
GUID.
|
||||
|
||||
**Data examples**
|
||||
|
||||
Partial data is allowed.
|
||||
|
||||
```json
|
||||
{
|
||||
"options": {"promoted": [
|
||||
["guid1", 10],
|
||||
["guid2", 5],
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Success Responses
|
||||
|
||||
**Condition** : Data provided is valid
|
||||
|
||||
**Code** : `200 OK`
|
||||
|
||||
**Content example** : Response will reflect a list of addon GUID suggestions.
|
||||
|
||||
```json
|
||||
{
|
||||
"results": ["taar-guid1", "taar-guid2", "taar-guid3"],
|
||||
"result_info": [],
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response
|
||||
|
||||
**Condition** : If provided data is invalid, e.g. options object is not a dictionary.
|
||||
|
||||
**Code** : `400 BAD REQUEST`
|
||||
|
||||
**Content example** :
|
||||
|
||||
```json
|
||||
{
|
||||
"invalid_option": [
|
||||
"Please provide a dictionary with a `promoted` key mapped to a list of promoted addon GUIDs",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
* Endpoint will ignore irrelevant and read-only data such as parameters that
|
||||
don't exist, or fields.
|
||||
* Endpoint will try to fail gracefully and return an empty list in the
|
||||
results key if no suggestions can be made.
|
||||
* The only condition when the endpoint should return an error code if
|
||||
the options data is malformed.
|
||||
|
||||
|
||||
|
|
@ -34,14 +34,37 @@ def configure_plugin(app): # noqa: C901
|
|||
This is a factory function that configures all the routes for
|
||||
flask given a particular library.
|
||||
"""
|
||||
@app.route('/api/recommendations/<uuid:uuid_client_id>/')
|
||||
def recommendations(uuid_client_id):
|
||||
|
||||
@app.route("/v1/api/recommendations/<hashed_client_id>/", methods=["GET", "POST"])
|
||||
def recommendations(hashed_client_id):
|
||||
"""Return a list of recommendations provided a telemetry client_id."""
|
||||
# Use the module global PROXY_MANAGER
|
||||
global PROXY_MANAGER
|
||||
|
||||
try:
|
||||
promoted_guids = []
|
||||
if request.method == "POST":
|
||||
json_data = request.data
|
||||
# At least Python3.5 returns request.data as bytes
|
||||
# type instead of a string type.
|
||||
# Both Python2.7 and Python3.7 return a string type
|
||||
if type(json_data) == bytes:
|
||||
json_data = json_data.decode("utf8")
|
||||
|
||||
post_data = json.loads(json_data)
|
||||
promoted_guids = post_data.get("options", {}).get("promoted", [])
|
||||
if promoted_guids:
|
||||
promoted_guids.sort(key=lambda x: x[1], reverse=True)
|
||||
promoted_guids = [x[0] for x in promoted_guids]
|
||||
except Exception as e:
|
||||
return app.response_class(
|
||||
response=json.dumps({"error": "Invalid JSON in POST: {}".format(e)}),
|
||||
status=400,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
# Coerce the uuid.UUID type into a string
|
||||
client_id = str(uuid_client_id)
|
||||
client_id = str(hashed_client_id)
|
||||
|
||||
branch = request.args.get("branch", "")
|
||||
|
||||
|
@ -76,6 +99,7 @@ def configure_plugin(app): # noqa: C901
|
|||
# Strip out weights from TAAR results to maintain compatibility
|
||||
# with TAAR 1.0
|
||||
jdata = {"results": [x[0] for x in recommendations]}
|
||||
jdata["results"] = (promoted_guids + jdata["results"])[:TAAR_MAX_RESULTS]
|
||||
|
||||
response = app.response_class(
|
||||
response=json.dumps(jdata), status=200, mimetype="application/json"
|
||||
|
|
|
@ -3,7 +3,15 @@ from taar.context import default_context
|
|||
from taar import ProfileFetcher
|
||||
from taar.profile_fetcher import ProfileController
|
||||
from taar import recommenders
|
||||
from flask import url_for
|
||||
import time
|
||||
from flask import Flask
|
||||
import uuid
|
||||
|
||||
try:
|
||||
from unittest.mock import MagicMock
|
||||
except Exception:
|
||||
from mock import MagicMock
|
||||
|
||||
|
||||
def create_recommendation_manager():
|
||||
|
@ -17,6 +25,44 @@ def create_recommendation_manager():
|
|||
return rm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
from taar.plugin import configure_plugin
|
||||
from taar.plugin import PROXY_MANAGER
|
||||
|
||||
flask_app = Flask("test")
|
||||
|
||||
# Clobber the default recommendation manager with a MagicMock
|
||||
mock_recommender = MagicMock()
|
||||
PROXY_MANAGER.setResource(mock_recommender)
|
||||
|
||||
configure_plugin(flask_app)
|
||||
|
||||
return flask_app
|
||||
|
||||
|
||||
def test_empty_results_by_default(client, app):
|
||||
# The default behaviour under test should be that the
|
||||
# RecommendationManager simply no-ops everything so we get back an
|
||||
# empty result list.
|
||||
res = client.get("/v1/api/recommendations/not_a_real_hash/")
|
||||
assert res.json == {"results": []}
|
||||
|
||||
|
||||
def test_only_promoted_addons(client, app):
|
||||
# POSTing a JSON blob allows us to specify promoted addons to the
|
||||
# TAAR service.
|
||||
res = client.post(
|
||||
"/v1/api/recommendations/not_a_real_hash/",
|
||||
json=dict(
|
||||
{"options": {"promoted": [["guid1", 10], ["guid2", 5], ["guid55", 8]]}}
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
# The result should order the GUIDs in descending order of weight
|
||||
assert res.json == {"results": ["guid1", "guid55", "guid2"]}
|
||||
|
||||
|
||||
@pytest.mark.skip("This is an integration test")
|
||||
def test_recommenders(client_id="some_dev_client_id", branch="linear"):
|
||||
"""
|
||||
|
@ -76,3 +122,79 @@ def micro_bench(x, client_id, branch_label):
|
|||
end = time.time()
|
||||
|
||||
print(("%0.5f seconds per request" % ((end - start) / x)))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_recommendation_manager(monkeypatch):
|
||||
# TODO: Clobbering the recommendationmanager really needs to be
|
||||
# simplified
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_empty_recommendation(client, empty_recommendation_manager):
|
||||
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
assert response.data == b'{"results": []}'
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_locale_recommendation(client, locale_recommendation_manager):
|
||||
response = client.get(
|
||||
url_for("recommendations", uuid_client_id=uuid.uuid4()) + "?locale=en-US"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
assert response.data == b'{"results": ["addon-Locale"]}'
|
||||
|
||||
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
assert response.data == b'{"results": []}'
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_platform_recommendation(client, platform_recommendation_manager):
|
||||
uri = (
|
||||
url_for("recommendations", uuid_client_id=str(uuid.uuid4())) + "?platform=WOW64"
|
||||
)
|
||||
response = client.get(uri)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
assert response.data == b'{"results": ["addon-WOW64"]}'
|
||||
|
||||
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
assert response.data == b'{"results": []}'
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_intervention_a(client, static_recommendation_manager):
|
||||
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
|
||||
response = client.get(url + "?branch=intervention-a")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
expected = b'{"results": ["intervention-a-addon-1", "intervention-a-addon-2", "intervention-a-addon-N"]}'
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_intervention_b(client, static_recommendation_manager):
|
||||
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
|
||||
response = client.get(url + "?branch=intervention_b")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
expected = b'{"results": ["intervention_b-addon-1", "intervention_b-addon-2", "intervention_b-addon-N"]}'
|
||||
assert response.data == expected
|
||||
|
||||
|
||||
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
|
||||
def test_control_branch(client, static_recommendation_manager):
|
||||
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
|
||||
response = client.get(url + "?branch=control")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "application/json"
|
||||
expected = b'{"results": ["control-addon-1", "control-addon-2", "control-addon-N"]}'
|
||||
assert response.data == expected
|
||||
|
|
Загрузка…
Ссылка в новой задаче