Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: upgrade to falcon 3.X #1296

Merged
merged 2 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ POSTGRES_HOST=postgres
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_EXPOSE=127.0.0.1:5432
# Expose postgres on localhost for dev
# POSTGRES_EXPOSE=127.0.0.1:5432

# Triton ML inference server
TRITON_HOST=triton
Expand Down Expand Up @@ -84,4 +85,8 @@ FASTTEXT_MODEL_DIR=./models
# Enable/disable MongoDB access. All insights/predictions are checked
# against MongoDB, we disable by default locally to be able to easily test
# image import
ENABLE_MONGODB_ACCESS=0
ENABLE_MONGODB_ACCESS=0

# gunicorn --auto-reload is not compatible with preload_app
# so it has to be disabled when developing, to allow hot reload
GUNICORN_PRELOAD_APP=0
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,5 @@ RUN \
mkdir -p /opt/robotoff/gh_pages /opt/robotoff/doc /opt/robotoff/.cov && \
chown -R off:off /opt/robotoff/gh_pages /opt/robotoff/doc /opt/robotoff/.cov
USER off
CMD [ "gunicorn", "--reload", "--config /opt/robotoff/gunicorn.py", "--log-file=-", "robotoff.app.api:api"]

1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ x-robotoff-base-env:
ROBOTOFF_TLD:
ROBOTOFF_SCHEME:
STATIC_DOMAIN:
GUNICORN_PRELOAD_APP:
GUNICORN_NUM_WORKERS:
ROBOTOFF_UPDATED_PRODUCT_WAIT:
REDIS_HOST:
Expand Down
5 changes: 4 additions & 1 deletion gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
# we have a trade-off with memory vs cpu numbers
workers = int(os.environ.get("GUNICORN_NUM_WORKERS", 4))
worker_connections = 1000
preload_app = True
# gunicorn --auto-reload is not compatible with preload_app
# so it has to be disabled when developing
# Default to True (production) if not specified
preload_app = bool(os.environ.get("GUNICORN_PRELOAD_APP", True))
timeout = 60
1,185 changes: 588 additions & 597 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ requests = "~2.31.0"
peewee = "~3.16.3"
psycopg2-binary = "~2.9.1"
gunicorn = "~20.1.0"
falcon = "~2.0.0"
falcon-cors = "~1.1.7"
falcon-multipart = "~0.2.0"
falcon = "~3.1.3"
elasticsearch = "~8.5.3"
pymongo = "~4.5.0"
dacite = "~1.6.0"
Expand Down
57 changes: 32 additions & 25 deletions robotoff/app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import peewee
import requests
from falcon.media.validators import jsonschema
from falcon_cors import CORS
from falcon_multipart.middleware import MultipartMiddleware
from openfoodfacts import OCRResult
from openfoodfacts.ocr import OCRParsingException, OCRResultGenerationException
from openfoodfacts.types import COUNTRY_CODE_TO_NAME, Country
Expand Down Expand Up @@ -111,8 +109,9 @@
) -> ServerType:
"""Get `ServerType` value from POST x-www-form-urlencoded or GET
requests."""
if req.media and "server_type" in req.media:
server_type_str = req.media["server_type"]
media = req.get_media(default_when_empty=None)
if media is not None and "server_type" in media:
server_type_str = media["server_type"]

Check warning on line 114 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L114

Added line #L114 was not covered by tests
else:
server_type_str = req.get_param("server_type")

Expand Down Expand Up @@ -578,26 +577,27 @@
def on_post(self, req: falcon.Request, resp: falcon.Response):
"""Predict categories using neural categorizer and matching algorithm
for a specific product."""
server_type: ServerType = ServerType[req.media.get("server_type", "off")]
server_type = get_server_type_from_req(req)

Check warning on line 580 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L580

Added line #L580 was not covered by tests

if server_type != ServerType.off:
raise falcon.HTTPBadRequest(
f"category predictor is only available for 'off' server type (here: '{server_type.name}')"
)

media = req.get_media()

Check warning on line 587 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L587

Added line #L587 was not covered by tests
neural_model_name = None
if (neural_model_name_str := req.media.get("neural_model_name")) is not None:
if (neural_model_name_str := media.get("neural_model_name")) is not None:

Check warning on line 589 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L589

Added line #L589 was not covered by tests
neural_model_name = NeuralCategoryClassifierModel[neural_model_name_str]

if "barcode" in req.media:
if "barcode" in media:

Check warning on line 592 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L592

Added line #L592 was not covered by tests
# Fetch product from DB
barcode: str = req.media["barcode"]
barcode: str = media["barcode"]

Check warning on line 594 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L594

Added line #L594 was not covered by tests
product = get_product(ProductIdentifier(barcode, server_type)) or {}
if not product:
raise falcon.HTTPNotFound(description=f"product {barcode} not found")
product_id = ProductIdentifier(barcode, server_type)
else:
product = req.media["product"]
product = media["product"]

Check warning on line 600 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L600

Added line #L600 was not covered by tests
product_id = ProductIdentifier("NULL", server_type)

if (ingredient_tags := product.pop("ingredients_tags", None)) is not None:
Expand All @@ -607,8 +607,8 @@
resp.media = predict_category(
product,
product_id,
deepest_only=req.media.get("deepest_only", False),
threshold=req.media.get("threshold"),
deepest_only=media.get("deepest_only", False),
threshold=media.get("threshold"),
neural_model_name=neural_model_name,
clear_cache=True, # clear resource cache to save memory
)
Expand Down Expand Up @@ -798,8 +798,9 @@
server_type = get_server_type_from_req(req)
timestamp = datetime.datetime.utcnow()
inserts = []
media = req.get_media()

Check warning on line 801 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L801

Added line #L801 was not covered by tests

for prediction in req.media["predictions"]:
for prediction in media["predictions"]:

Check warning on line 803 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L803

Added line #L803 was not covered by tests
product_id = ProductIdentifier(prediction["barcode"], server_type)
source_image = generate_image_path(product_id, prediction.pop("image_id"))
inserts.append(
Expand Down Expand Up @@ -1044,6 +1045,7 @@
@jsonschema.validate(schema.UPDATE_LOGO_SCHEMA)
def on_put(self, req: falcon.Request, resp: falcon.Response, logo_id: int):
auth = parse_auth(req)
media = req.get_media()

Check warning on line 1048 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L1048

Added line #L1048 was not covered by tests
if auth is None:
raise falcon.HTTPForbidden(
description="authentication is required to annotate logos"
Expand All @@ -1055,8 +1057,8 @@
resp.status = falcon.HTTP_404
return

type_ = req.media["type"]
value = req.media.get("value") or None
type_ = media["type"]
value = media.get("value") or None

Check warning on line 1061 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L1060-L1061

Added lines #L1060 - L1061 were not covered by tests
check_logo_annotation(type_, value)

if type_ != logo.annotation_type or value != logo.annotation_value:
Expand Down Expand Up @@ -1127,12 +1129,13 @@
@jsonschema.validate(schema.ANNOTATE_LOGO_SCHEMA)
def on_post(self, req: falcon.Request, resp: falcon.Response):
auth = parse_auth(req)
media = req.get_media()
if auth is None:
raise falcon.HTTPForbidden(
description="authentication is required to annotate logos"
)
server_type: ServerType = ServerType[req.media.get("server_type", "off")]
annotations = req.media["annotations"]
server_type = get_server_type_from_req(req)
annotations = media["annotations"]
completed_at = datetime.datetime.utcnow()
annotation_logos = []

Expand Down Expand Up @@ -1800,23 +1803,27 @@
resp.status = falcon.HTTP_200


cors = CORS(
allow_all_origins=True,
allow_all_headers=True,
allow_all_methods=True,
allow_credentials_all_origins=True,
max_age=600,
)
def custom_handle_uncaught_exception(
req: falcon.Request, resp: falcon.Response, ex: Exception, params
):
"""Handle uncaught exceptions and log them. Return a 500 error to the
client."""
raise falcon.HTTPInternalServerError(description=str(ex))

Check warning on line 1811 in robotoff/app/api.py

View check run for this annotation

Codecov / codecov/patch

robotoff/app/api.py#L1811

Added line #L1811 was not covered by tests


api = falcon.API(
middleware=[cors.middleware, MultipartMiddleware(), DBConnectionMiddleware()]
api = falcon.App(
middleware=[
falcon.CORSMiddleware(allow_origins="*", allow_credentials="*"),
DBConnectionMiddleware(),
],
)

json_handler = falcon.media.JSONHandler(dumps=orjson.dumps, loads=orjson.loads)
extra_handlers = {
"application/json": json_handler,
}

api.req_options.media_handlers.update(extra_handlers)
api.resp_options.media_handlers.update(extra_handlers)

# Parse form parameters
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime

import pytest
import requests
from falcon import testing

from robotoff.app import events
Expand Down Expand Up @@ -1252,6 +1253,18 @@ def test_predict_lang(client, mocker):
assert result.json == {"predictions": expected_predictions}


def test_predict_lang_http_error(client, mocker):
mocker.patch(
"robotoff.app.api.predict_lang",
side_effect=requests.exceptions.ConnectionError("A connection error occurred"),
)
result = client.simulate_get(
"/api/v1/predict/lang", params={"text": "hello", "k": 2}
)
assert result.status_code == 500
assert result.json == {"title": "500 Internal Server Error"}


def test_predict_product_language(client, peewee_db):
barcode = "123456789"
prediction_data_1 = {"count": {"en": 10, "fr": 5, "es": 3, "words": 18}}
Expand Down
Loading