From 4b4e6214eaac2abbea448a03e13e27af553ada6b Mon Sep 17 00:00:00 2001 From: Juho Inkinen Date: Fri, 28 Apr 2023 15:31:42 +0300 Subject: [PATCH 01/28] Upgrade to Connexion 3 alpha-6 --- annif/__init__.py | 18 ++++++++++-------- pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index f4a5831f5..c1a1d3086 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -30,19 +30,21 @@ def create_app(config_name=None): import connexion from flask_cors import CORS - from annif.openapi.validation import CustomRequestBodyValidator + import annif.registry + + # from annif.openapi.validation import CustomRequestBodyValidator # TODO Re-enable specdir = os.path.join(os.path.dirname(__file__), "openapi") - cxapp = connexion.App(__name__, specification_dir=specdir) + cxapp = connexion.FlaskApp(__name__, specification_dir=specdir) config_name = _get_config_name(config_name) logger.debug(f"creating connexion app with configuration {config_name}") cxapp.app.config.from_object(config_name) cxapp.app.config.from_envvar("ANNIF_SETTINGS", silent=True) - validator_map = { - "body": CustomRequestBodyValidator, - } - cxapp.add_api("annif.yaml", validator_map=validator_map) + # validator_map = { + # "body": CustomRequestBodyValidator, + # } + cxapp.add_api("annif.yaml") # validator_map=validator_map) # add CORS support CORS(cxapp.app) @@ -56,8 +58,8 @@ def create_app(config_name=None): cxapp.app.register_blueprint(bp) - # return the Flask app - return cxapp.app + # return the Connexion app + return cxapp def _get_config_name(config_name): diff --git a/pyproject.toml b/pyproject.toml index 1dba9286d..136c5bc75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers=[ [tool.poetry.dependencies] python = ">=3.8,<3.11" -connexion = {version = "2.14.*", extras = ["swagger-ui"]} +connexion = {version = "3.0.0a6", allow-prereleases = true, extras = ["flask","uvicorn", "swagger-ui"]} flask = ">=1.0.4,<3" flask-cors = "3.0.*" click = "8.1.*" From 57fcaa95ebc57c045dcc8c4efe407095b2f68575 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Tue, 9 May 2023 22:04:57 +0300 Subject: [PATCH 02/28] Make application/x-www-form-urlencoded request with axios --- annif/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annif/templates/home.html b/annif/templates/home.html index 1e732366d..551d1eb63 100644 --- a/annif/templates/home.html +++ b/annif/templates/home.html @@ -296,7 +296,7 @@

Suggested subjects

\ return; } var this_ = this; - var formData = new FormData(); + var formData = new URLSearchParams(); formData.append('text', this_.text); formData.append('limit', this_.limit); this_.loading = true; From f046c1f0ed3812ea34d0acba7fcfe118022d2f9c Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Tue, 9 May 2023 22:37:03 +0300 Subject: [PATCH 03/28] Fix OpenAPI spec for /learn endpoint --- annif/openapi/annif.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/annif/openapi/annif.yaml b/annif/openapi/annif.yaml index c5143313d..74e8a4661 100644 --- a/annif/openapi/annif.yaml +++ b/annif/openapi/annif.yaml @@ -174,7 +174,9 @@ paths: responses: "204": description: successful operation - content: {} + content: + application/json: + {} "404": $ref: '#/components/responses/NotFound' "503": From 6d0a68fc4582d14393fa7abb97361befe5e8f53c Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 3 May 2023 15:18:03 +0300 Subject: [PATCH 04/28] Set content-types in response headers in rest methods --- annif/rest.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/annif/rest.py b/annif/rest.py index 0b3b87efe..f84bd8342 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -33,7 +33,8 @@ def server_error(err): def show_info(): """return version of annif and a title for the api according to OpenAPI spec""" - return {"title": "Annif REST API", "version": importlib.metadata.version("annif")} + result = {"title": "Annif REST API", "version": importlib.metadata.version("annif")} + return result, 200, {"Content-Type": "application/json"} def language_not_supported_error(lang): @@ -49,12 +50,13 @@ def language_not_supported_error(lang): def list_projects(): """return a dict with projects formatted according to OpenAPI spec""" - return { + result = { "projects": [ proj.dump() for proj in annif.registry.get_projects(min_access=Access.public).values() ] } + return result, 200, {"Content-Type": "application/json"} def show_project(project_id): @@ -64,7 +66,7 @@ def show_project(project_id): project = annif.registry.get_project(project_id, min_access=Access.hidden) except ValueError: return project_not_found_error(project_id) - return project.dump() + return project.dump(), 200, {"Content-Type": "application/json"} def _suggestion_to_dict(suggestion, subject_index, language): @@ -103,7 +105,7 @@ def suggest(project_id, body): if _is_error(result): return result - return result[0] + return result[0], 200, {"Content-Type": "application/json"} def suggest_batch(project_id, body, **query_parameters): @@ -117,7 +119,7 @@ def suggest_batch(project_id, body, **query_parameters): return result for document_results, document in zip(result, documents): document_results["document_id"] = document.get("document_id") - return result + return result, 200, {"Content-Type": "application/json"} def _suggest(project_id, documents, parameters): @@ -179,4 +181,4 @@ def learn(project_id, body): except AnnifException as err: return server_error(err) - return None, 204 + return None, 204, {"Content-Type": "application/json"} From c31563350eb8dfe0bf9589cc9172556e157394a4 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 10 May 2023 10:59:34 +0300 Subject: [PATCH 05/28] Adapt tests for rest returning also status codes & content types --- tests/test_rest.py | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/tests/test_rest.py b/tests/test_rest.py index e56a24b21..c905fc1de 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -7,7 +7,7 @@ def test_rest_list_projects(app): with app.app_context(): - result = annif.rest.list_projects() + result = annif.rest.list_projects()[0] project_ids = [proj["project_id"] for proj in result["projects"]] # public project should be returned assert "dummy-fi" in project_ids @@ -21,7 +21,7 @@ def test_rest_list_projects(app): def test_rest_show_info(app): with app.app_context(): - result = annif.rest.show_info() + result = annif.rest.show_info()[0] version = importlib.metadata.version("annif") assert result == {"title": "Annif REST API", "version": version} @@ -29,14 +29,14 @@ def test_rest_show_info(app): def test_rest_show_project_public(app): # public projects should be accessible via REST with app.app_context(): - result = annif.rest.show_project("dummy-fi") + result = annif.rest.show_project("dummy-fi")[0] assert result["project_id"] == "dummy-fi" def test_rest_show_project_hidden(app): # hidden projects should be accessible if you know the project id with app.app_context(): - result = annif.rest.show_project("dummy-en") + result = annif.rest.show_project("dummy-en")[0] assert result["project_id"] == "dummy-en" @@ -58,7 +58,7 @@ def test_rest_suggest_public(app): with app.app_context(): result = annif.rest.suggest( "dummy-fi", {"text": "example text", "limit": 10, "threshold": 0.0} - ) + )[0] assert "results" in result @@ -67,7 +67,7 @@ def test_rest_suggest_hidden(app): with app.app_context(): result = annif.rest.suggest( "dummy-en", {"text": "example text", "limit": 10, "threshold": 0.0} - ) + )[0] assert "results" in result @@ -101,7 +101,7 @@ def test_rest_suggest_with_language_override(app): result = annif.rest.suggest( "dummy-vocablang", {"text": "example text", "limit": 10, "threshold": 0.0, "language": "en"}, - ) + )[0] assert result["results"][0]["label"] == "dummy" @@ -120,7 +120,7 @@ def test_rest_suggest_with_different_vocab_language(app): with app.app_context(): result = annif.rest.suggest( "dummy-vocablang", {"text": "example text", "limit": 10, "threshold": 0.0} - ) + )[0] assert result["results"][0]["label"] == "dummy-fi" @@ -128,7 +128,7 @@ def test_rest_suggest_with_notations(app): with app.app_context(): result = annif.rest.suggest( "dummy-fi", {"text": "example text", "limit": 10, "threshold": 0.0} - ) + )[0] assert result["results"][0]["notation"] is None @@ -136,7 +136,7 @@ def test_rest_suggest_batch_one_doc(app): with app.app_context(): result = annif.rest.suggest_batch( "dummy-fi", {"documents": [{"text": "example text"}]} - ) + )[0] assert len(result) == 1 assert result[0]["results"][0]["label"] == "dummy-fi" assert result[0]["document_id"] is None @@ -147,7 +147,7 @@ def test_rest_suggest_batch_one_doc_with_id(app): result = annif.rest.suggest_batch( "dummy-fi", {"documents": [{"text": "example text", "document_id": "doc-0"}]}, - ) + )[0] assert len(result) == 1 assert result[0]["results"][0]["label"] == "dummy-fi" assert result[0]["document_id"] == "doc-0" @@ -163,7 +163,7 @@ def test_rest_suggest_batch_two_docs(app): {"text": "another example text"}, ] }, - ) + )[0] assert len(result) == 2 assert result[1]["results"][0]["label"] == "dummy-fi" @@ -176,7 +176,7 @@ def test_rest_suggest_batch_with_language_override(app): "documents": [{"text": "example text"}], }, language="en", - ) + )[0] assert result[0]["results"][0]["label"] == "dummy" @@ -188,14 +188,18 @@ def test_rest_suggest_batch_with_limit_override(app): "documents": [{"text": "example text"}], }, limit=0, - ) + )[0] assert len(result[0]["results"]) == 0 def test_rest_learn_empty(app): with app.app_context(): response = annif.rest.learn("dummy-en", []) - assert response == (None, 204) # success, no output + assert response == ( + None, + 204, + {"Content-Type": "application/json"}, + ) # success, no output def test_rest_learn(app): @@ -207,11 +211,15 @@ def test_rest_learn(app): ] with app.app_context(): response = annif.rest.learn("dummy-en", documents) - assert response == (None, 204) # success, no output + assert response == ( + None, + 204, + {"Content-Type": "application/json"}, + ) # success, no output result = annif.rest.suggest( "dummy-en", {"text": "example text", "limit": 10, "threshold": 0.0} - ) + )[0] assert "results" in result assert result["results"][0]["uri"] == "http://example.org/none" assert result["results"][0]["label"] == "none" From e417e02198a0a4203d7805a35f33176e0cfc681c Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 10 May 2023 11:01:29 +0300 Subject: [PATCH 06/28] Adapt tests and fixtures for using Connexion app --- tests/conftest.py | 21 +++++++++++++-------- tests/test_openapi.py | 26 +++++++++++++------------- tests/test_project.py | 16 ++++++++-------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fcccb268f..f2e84e9da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,27 +15,32 @@ @pytest.fixture(scope="module") -def app(): +def cxapp(): # make sure the dummy vocab is in place because many tests depend on it subjfile = os.path.join(os.path.dirname(__file__), "corpora", "dummy-subjects.csv") - app = annif.create_app(config_name="annif.default_config.TestingConfig") - with app.app_context(): + cxapp = annif.create_app(config_name="annif.default_config.TestingConfig") + with cxapp.app.app_context(): project = annif.registry.get_project("dummy-en") # the vocab is needed for both English and Finnish language projects vocab = annif.corpus.SubjectFileCSV(subjfile) project.vocab.load_vocabulary(vocab) - return app + return cxapp + + +@pytest.fixture(scope="module") +def app(cxapp): + return cxapp.app @pytest.fixture(scope="module") def app_with_initialize(): - app = annif.create_app(config_name="annif.default_config.TestingInitializeConfig") - return app + cxapp = annif.create_app(config_name="annif.default_config.TestingInitializeConfig") + return cxapp.app @pytest.fixture -def app_client(app): - with app.test_client() as app_client: +def app_client(cxapp): + with cxapp.test_client() as app_client: yield app_client diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 26e33e4ea..6ffe0e52c 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -14,8 +14,8 @@ def check_cors(response, case): @schema.parametrize() @settings(max_examples=10) -def test_openapi_fuzzy(case, app): - response = case.call_wsgi(app) +def test_openapi_fuzzy(case, cxapp): + response = case.call_asgi(cxapp) case.validate_response(response, additional_checks=(check_cors,)) @@ -31,13 +31,13 @@ def test_openapi_fuzzy_target_dummy_fi(case, app): def test_openapi_list_projects(app_client): req = app_client.get("http://localhost:8000/v1/projects") assert req.status_code == 200 - assert "projects" in req.get_json() + assert "projects" in req.json() def test_openapi_show_project(app_client): req = app_client.get("http://localhost:8000/v1/projects/dummy-fi") assert req.status_code == 200 - assert req.get_json()["project_id"] == "dummy-fi" + assert req.json()["project_id"] == "dummy-fi" def test_openapi_show_project_nonexistent(app_client): @@ -51,7 +51,7 @@ def test_openapi_suggest(app_client): "http://localhost:8000/v1/projects/dummy-fi/suggest", data=data ) assert req.status_code == 200 - assert "results" in req.get_json() + assert "results" in req.json() def test_openapi_suggest_nonexistent(app_client): @@ -76,18 +76,18 @@ def test_openapi_suggest_batch(app_client): "http://localhost:8000/v1/projects/dummy-fi/suggest-batch", json=data ) assert req.status_code == 200 - body = req.get_json() + body = req.json() assert len(body) == 32 assert body[0]["results"][0]["label"] == "dummy-fi" -def test_openapi_suggest_batch_too_many_documents(app_client): - data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}] * 33} - req = app_client.post( - "http://localhost:8000/v1/projects/dummy-fi/suggest-batch", json=data - ) - assert req.status_code == 400 - assert req.get_json()["detail"] == "too many items - 'documents'" +# def test_openapi_suggest_batch_too_many_documents(app_client): +# data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}]*33} +# req = app_client.post( +# "http://localhost:8000/v1/projects/dummy-fi/suggest-batch", json=data +# ) +# assert req.status_code == 400 +# assert req.json()["detail"] == "too many items - 'documents'" def test_openapi_learn(app_client): diff --git a/tests/test_project.py b/tests/test_project.py index 6600d664c..ac308ae9a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -132,10 +132,10 @@ def test_get_project_default_params_fasttext(registry): def test_get_project_invalid_config_file(): - app = annif.create_app( + cxapp = annif.create_app( config_name="annif.default_config.TestingInvalidProjectsConfig" ) - with app.app_context(): + with cxapp.app.app_context(): with pytest.raises(ConfigurationException): annif.registry.get_project("duplicatedvocab") @@ -285,23 +285,23 @@ def test_project_initialized(app_with_initialize): def test_project_file_not_found(): - app = annif.create_app(config_name="annif.default_config.TestingNoProjectsConfig") - with app.app_context(): + cxapp = annif.create_app(config_name="annif.default_config.TestingNoProjectsConfig") + with cxapp.app.app_context(): with pytest.raises(ValueError): annif.registry.get_project("dummy-en") def test_project_file_toml(): - app = annif.create_app(config_name="annif.default_config.TestingTOMLConfig") - with app.app_context(): + cxapp = annif.create_app(config_name="annif.default_config.TestingTOMLConfig") + with cxapp.app.app_context(): assert len(annif.registry.get_projects()) == 2 assert annif.registry.get_project("dummy-fi-toml").project_id == "dummy-fi-toml" assert annif.registry.get_project("dummy-en-toml").project_id == "dummy-en-toml" def test_project_directory(): - app = annif.create_app(config_name="annif.default_config.TestingDirectoryConfig") - with app.app_context(): + cxapp = annif.create_app(config_name="annif.default_config.TestingDirectoryConfig") + with cxapp.app.app_context(): assert len(annif.registry.get_projects()) == 18 + 2 assert annif.registry.get_project("dummy-fi").project_id == "dummy-fi" assert annif.registry.get_project("dummy-fi-toml").project_id == "dummy-fi-toml" From 9cd6a608d5f2778a144cef90c9f671012d26fc55 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:54:53 +0200 Subject: [PATCH 07/28] Upgrade to Connexion 3.0.* (from alpha); remove direct Flask dependency --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 136c5bc75..16656bed8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,7 @@ classifiers=[ [tool.poetry.dependencies] python = ">=3.8,<3.11" -connexion = {version = "3.0.0a6", allow-prereleases = true, extras = ["flask","uvicorn", "swagger-ui"]} -flask = ">=1.0.4,<3" +connexion = {version = "3.0.*", extras = ["flask","uvicorn", "swagger-ui"]} flask-cors = "3.0.*" click = "8.1.*" click-log = "0.4.*" From 067983b7160fd4ffed3e89817cbbbca747f4f520 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:26:18 +0200 Subject: [PATCH 08/28] Drop using flask-cors (TODO use CORSMiddleware) --- annif/__init__.py | 6 ++---- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index 3e0c26ea5..f9fd313c7 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -33,11 +33,9 @@ def create_flask_app(config_name: str | None = None) -> Flask: def create_app(config_name: str | None = None) -> Flask: """Create a Connexion app to be used for the API.""" - # 'cxapp' here is the Connexion application that has a normal Flask app - # as a property (cxapp.app) import connexion - from flask_cors import CORS + # from flask_cors import CORS # TODO Use CORSMiddleware import annif.registry # from annif.openapi.validation import CustomRequestBodyValidator # TODO Re-enable @@ -55,7 +53,7 @@ def create_app(config_name: str | None = None) -> Flask: cxapp.add_api("annif.yaml") # validator_map=validator_map) # add CORS support - CORS(cxapp.app) + # CORS(cxapp.app) if cxapp.app.config["INITIALIZE_PROJECTS"]: annif.registry.initialize_projects(cxapp.app) diff --git a/pyproject.toml b/pyproject.toml index 2cda07895..acd7bab83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ classifiers=[ python = ">=3.8,<3.12" connexion = {version = "3.0.*", extras = ["flask","uvicorn", "swagger-ui"]} -flask-cors = "4.0.*" click = "8.1.*" click-log = "0.4.*" joblib = "1.3.*" From d485a3f02f2fe2b404733b6cb7352730ad6abba1 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:26:18 +0200 Subject: [PATCH 09/28] Drop using flask-cors (TODO use CORSMiddleware) --- annif/__init__.py | 6 ++---- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index 3e0c26ea5..f9fd313c7 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -33,11 +33,9 @@ def create_flask_app(config_name: str | None = None) -> Flask: def create_app(config_name: str | None = None) -> Flask: """Create a Connexion app to be used for the API.""" - # 'cxapp' here is the Connexion application that has a normal Flask app - # as a property (cxapp.app) import connexion - from flask_cors import CORS + # from flask_cors import CORS # TODO Use CORSMiddleware import annif.registry # from annif.openapi.validation import CustomRequestBodyValidator # TODO Re-enable @@ -55,7 +53,7 @@ def create_app(config_name: str | None = None) -> Flask: cxapp.add_api("annif.yaml") # validator_map=validator_map) # add CORS support - CORS(cxapp.app) + # CORS(cxapp.app) if cxapp.app.config["INITIALIZE_PROJECTS"]: annif.registry.initialize_projects(cxapp.app) diff --git a/pyproject.toml b/pyproject.toml index 2cda07895..acd7bab83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ classifiers=[ python = ">=3.8,<3.12" connexion = {version = "3.0.*", extras = ["flask","uvicorn", "swagger-ui"]} -flask-cors = "4.0.*" click = "8.1.*" click-log = "0.4.*" joblib = "1.3.*" From 39027488af3c4b285746249e3e9e2ff2f87a9b5c Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:26:46 +0200 Subject: [PATCH 10/28] Add annif run CLI command for starting uvicorn dev server --- annif/__init__.py | 10 +++++++--- annif/cli.py | 27 +++++++++++++++++++-------- docs/source/commands.rst | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index f9fd313c7..aa44f223a 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -7,6 +7,8 @@ import os.path from typing import TYPE_CHECKING +from flask import Flask + logging.basicConfig() logger = logging.getLogger("annif") logger.setLevel(level=logging.INFO) @@ -14,12 +16,11 @@ import annif.backend # noqa if TYPE_CHECKING: - from flask.app import Flask + from connexion.apps.flask import FlaskApp def create_flask_app(config_name: str | None = None) -> Flask: """Create a Flask app to be used by the CLI.""" - from flask import Flask _set_tensorflow_loglevel() @@ -31,7 +32,7 @@ def create_flask_app(config_name: str | None = None) -> Flask: return app -def create_app(config_name: str | None = None) -> Flask: +def create_cx_app(config_name: str | None = None) -> FlaskApp: """Create a Connexion app to be used for the API.""" import connexion @@ -68,6 +69,9 @@ def create_app(config_name: str | None = None) -> Flask: return cxapp +create_app = create_cx_app # Alias to allow starting directly with uvicorn run + + def _get_config_name(config_name: str | None) -> str: if config_name is None: config_name = os.environ.get("ANNIF_CONFIG") diff --git a/annif/cli.py b/annif/cli.py index 73f18f02e..bb8c31304 100644 --- a/annif/cli.py +++ b/annif/cli.py @@ -26,14 +26,10 @@ logger = annif.logger click_log.basic_config(logger) - -if len(sys.argv) > 1 and sys.argv[1] in ("run", "routes"): - create_app = annif.create_app # Use Flask with Connexion -else: - # Connexion is not needed for most CLI commands, use plain Flask - create_app = annif.create_flask_app - -cli = FlaskGroup(create_app=create_app, add_version_option=False) +create_app = annif.create_flask_app +cli = FlaskGroup( + create_app=create_app, add_default_commands=False, add_version_option=False +) cli = click.version_option(message="%(version)s")(cli) @@ -443,6 +439,21 @@ def run_eval( ) +@cli.command("run") +@click.option("--port", type=int) +@click.option("--log-level") +@click_log.simple_verbosity_option(logger, default="ERROR") +def run_app(**kwargs): + """ + Run Annif in server mode for development. + \f + The server is for development purposes only. + """ + kwargs = {k: v for k, v in kwargs.items() if v is not None} + cxapp = annif.create_cx_app() + cxapp.run(**kwargs) + + FILTER_BATCH_MAX_LIMIT = 15 OPTIMIZE_METRICS = ["Precision (doc avg)", "Recall (doc avg)", "F1 score (doc avg)"] diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 849f6aadf..a76ea8b7c 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -121,7 +121,7 @@ Subject index administration N/A -.. click:: flask.cli:run_command +.. click:: annif.cli:run_app :prog: annif run **REST equivalent** From aee036f947c3fc5473b889f03650f21e13de36d7 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:02:24 +0200 Subject: [PATCH 11/28] Require connexion version >= 3.0.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 04db779e7..49fbbfc31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers=[ [tool.poetry.dependencies] python = ">=3.8,<3.12" -connexion = {version = "3.0.*", extras = ["flask","uvicorn", "swagger-ui"]} +connexion = {version = ">=3.0.5", extras = ["flask","uvicorn", "swagger-ui"]} click = "8.1.*" click-log = "0.4.*" joblib = "1.3.*" From beac55c17c15ceee362b86d025fa5fb31e1a001d Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:03:21 +0300 Subject: [PATCH 12/28] Re-enable CORS --- annif/__init__.py | 8 +++++++- pyproject.toml | 2 +- tests/test_openapi.py | 16 ++++++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index aa44f223a..46c91ab99 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -35,6 +35,8 @@ def create_flask_app(config_name: str | None = None) -> Flask: def create_cx_app(config_name: str | None = None) -> FlaskApp: """Create a Connexion app to be used for the API.""" import connexion + from connexion.middleware import MiddlewarePosition + from starlette.middleware.cors import CORSMiddleware # from flask_cors import CORS # TODO Use CORSMiddleware import annif.registry @@ -54,7 +56,11 @@ def create_cx_app(config_name: str | None = None) -> FlaskApp: cxapp.add_api("annif.yaml") # validator_map=validator_map) # add CORS support - # CORS(cxapp.app) + cxapp.add_middleware( + CORSMiddleware, + position=MiddlewarePosition.BEFORE_EXCEPTION, + allow_origins=["*"], + ) if cxapp.app.config["INITIALIZE_PROJECTS"]: annif.registry.initialize_projects(cxapp.app) diff --git a/pyproject.toml b/pyproject.toml index 65e736107..d47986622 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.9,<3.12" -connexion = {version = ">=3.0.5", extras = ["flask","uvicorn", "swagger-ui"]} +connexion = {version = ">=3.0.5", extras = ["flask", "uvicorn", "swagger-ui"]} click = "8.1.*" click-log = "0.4.*" joblib = "1.3.*" diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 6ffe0e52c..0c98807d4 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -7,16 +7,11 @@ schema = schemathesis.from_path("annif/openapi/annif.yaml") -@schemathesis.check -def check_cors(response, case): - assert response.headers["access-control-allow-origin"] == "*" - - @schema.parametrize() @settings(max_examples=10) def test_openapi_fuzzy(case, cxapp): response = case.call_asgi(cxapp) - case.validate_response(response, additional_checks=(check_cors,)) + case.validate_response(response) @pytest.mark.slow @@ -28,6 +23,15 @@ def test_openapi_fuzzy_target_dummy_fi(case, app): case.validate_response(response) +def test_openapi_cors(app_client): + # test that the service supports CORS by simulating a cross-origin request + app_client.headers = {"Origin": "http://somedomain.com"} + req = app_client.get( + "http://localhost:8000/v1/projects", + ) + assert req.headers["access-control-allow-origin"] == "*" + + def test_openapi_list_projects(app_client): req = app_client.get("http://localhost:8000/v1/projects") assert req.status_code == 200 From 556a29a55af6459f818814f754ae6d241eb47912 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:43:18 +0300 Subject: [PATCH 13/28] Exclude fuzzy cases where path parameters contain newline "%0A" --- tests/test_openapi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 0c98807d4..9b872c79c 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -7,6 +7,15 @@ schema = schemathesis.from_path("annif/openapi/annif.yaml") +@schemathesis.hook("filter_path_parameters") +def filter_path_parameters(context, path_parameters): + # Exclude path parameters containing newline which crashes application + # https://github.com/spec-first/connexion/issues/1908 + if path_parameters is not None and "project_id" in path_parameters: + return "%0A" not in path_parameters["project_id"] + return True + + @schema.parametrize() @settings(max_examples=10) def test_openapi_fuzzy(case, cxapp): From f4c28dc09cfdf690567d9c580b5c5e02974a306b Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:52:31 +0300 Subject: [PATCH 14/28] Adapt tests for removed "annif routes" and customized "annif run" command --- tests/test_cli.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 77adeab0f..1ea790369 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1055,21 +1055,7 @@ def test_run(): result = runner.invoke(annif.cli.cli, ["run", "--help"]) assert not result.exception assert result.exit_code == 0 - assert "Run a local development server." in result.output - - -def test_routes_with_flask_app(): - # When using plain Flask only the static endpoint exists - result = runner.invoke(annif.cli.cli, ["routes"]) - assert re.search(r"static\s+GET\s+\/static\/\", result.output) - assert not re.search(r"app.home\s+GET\s+\/", result.output) - - -def test_routes_with_connexion_app(): - # When using Connexion all endpoints exist - result = os.popen("python annif/cli.py routes").read() - assert re.search(r"static\s+GET\s+\/static\/", result) - assert re.search(r"app.home\s+GET\s+\/", result) + assert "Run Annif in server mode for development." in result.output def test_completion_script_generation(): From a0a72462e88b4a0a929ba4cb458f00aae926b4ec Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:01:13 +0300 Subject: [PATCH 15/28] Fix slow fuzzy test by making it use cxapp and asgi test calls --- tests/test_openapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 9b872c79c..5d0f9b05c 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -26,9 +26,9 @@ def test_openapi_fuzzy(case, cxapp): @pytest.mark.slow @schema.parametrize(endpoint="/v1/projects/{project_id}") @settings(max_examples=50) -def test_openapi_fuzzy_target_dummy_fi(case, app): +def test_openapi_fuzzy_target_dummy_fi(case, cxapp): case.path_parameters = {"project_id": "dummy-fi"} - response = case.call_wsgi(app) + response = case.call_asgi(cxapp) case.validate_response(response) From f12ca5e8278d5f79d10af49f694692fea2921ca5 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:45:19 +0300 Subject: [PATCH 16/28] Test with mocking that run command tries to start up server --- tests/test_cli.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1ea790369..6ab07c11f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1051,7 +1051,15 @@ def test_version_option(): assert result.output.strip() == version.strip() -def test_run(): +@mock.patch("connexion.FlaskApp.run") +def test_run(run): + result = runner.invoke(annif.cli.cli, ["run"]) + assert not result.exception + assert result.exit_code == 0 + assert run.called + + +def test_run_help(): result = runner.invoke(annif.cli.cli, ["run", "--help"]) assert not result.exception assert result.exit_code == 0 From 3703423190af6eb71f2d60331023a73a7c70205f Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:56:39 +0300 Subject: [PATCH 17/28] Update CustomRequestBodyValidator for Connexion 3 and re-enable its test --- annif/__init__.py | 20 +++++++++++++------- annif/openapi/validation.py | 35 +++++++++++++---------------------- tests/test_openapi.py | 14 +++++++------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/annif/__init__.py b/annif/__init__.py index 46c91ab99..120c7d4cf 100644 --- a/annif/__init__.py +++ b/annif/__init__.py @@ -35,13 +35,13 @@ def create_flask_app(config_name: str | None = None) -> Flask: def create_cx_app(config_name: str | None = None) -> FlaskApp: """Create a Connexion app to be used for the API.""" import connexion + from connexion.datastructures import MediaTypeDict from connexion.middleware import MiddlewarePosition + from connexion.validators import FormDataValidator, MultiPartFormDataValidator from starlette.middleware.cors import CORSMiddleware - # from flask_cors import CORS # TODO Use CORSMiddleware import annif.registry - - # from annif.openapi.validation import CustomRequestBodyValidator # TODO Re-enable + from annif.openapi.validation import CustomRequestBodyValidator specdir = os.path.join(os.path.dirname(__file__), "openapi") cxapp = connexion.FlaskApp(__name__, specification_dir=specdir) @@ -50,10 +50,16 @@ def create_cx_app(config_name: str | None = None) -> FlaskApp: cxapp.app.config.from_object(config_name) cxapp.app.config.from_envvar("ANNIF_SETTINGS", silent=True) - # validator_map = { - # "body": CustomRequestBodyValidator, - # } - cxapp.add_api("annif.yaml") # validator_map=validator_map) + validator_map = { + "body": MediaTypeDict( + { + "*/*json": CustomRequestBodyValidator, + "application/x-www-form-urlencoded": FormDataValidator, + "multipart/form-data": MultiPartFormDataValidator, + } + ), + } + cxapp.add_api("annif.yaml", validator_map=validator_map) # add CORS support cxapp.add_middleware( diff --git a/annif/openapi/validation.py b/annif/openapi/validation.py index e57d6830d..5cf4da392 100644 --- a/annif/openapi/validation.py +++ b/annif/openapi/validation.py @@ -3,48 +3,39 @@ from __future__ import annotations import logging +from typing import Any -import jsonschema -from connexion import decorators from connexion.exceptions import BadRequestProblem -from connexion.utils import is_null +from connexion.json_schema import format_error_with_path +from connexion.validators import JSONRequestBodyValidator +from jsonschema.exceptions import ValidationError logger = logging.getLogger("openapi.validation") -class CustomRequestBodyValidator(decorators.validation.RequestBodyValidator): +class CustomRequestBodyValidator(JSONRequestBodyValidator): """Custom request body validator that overrides the default error message for the 'maxItems' validator for the 'documents' property.""" def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def validate_schema( - self, - data: list | dict, - url: str, - ) -> None: - """Validate the request body against the schema.""" - - if self.is_null_value_valid and is_null(data): - return None # pragma: no cover - + def _validate(self, body: Any) -> dict | None: + if not self._nullable and body is None: + raise BadRequestProblem("Request body must not be empty") try: - self.validator.validate(data) - except jsonschema.ValidationError as exception: + return self._validator.validate(body) + except ValidationError as exception: + # Prevent logging request body with contents of all documents if exception.validator == "maxItems" and list(exception.schema_path) == [ "properties", "documents", "maxItems", ]: exception.message = "too many items" - - error_path_msg = self._error_path_message(exception=exception) + error_path_msg = format_error_with_path(exception=exception) logger.error( - "{url} validation error: {error}{error_path_msg}".format( - url=url, error=exception.message, error_path_msg=error_path_msg - ), + f"Validation error: {exception.message}{error_path_msg}", extra={"validator": "body"}, ) raise BadRequestProblem(detail=f"{exception.message}{error_path_msg}") - return None diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 5d0f9b05c..76f33695f 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -94,13 +94,13 @@ def test_openapi_suggest_batch(app_client): assert body[0]["results"][0]["label"] == "dummy-fi" -# def test_openapi_suggest_batch_too_many_documents(app_client): -# data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}]*33} -# req = app_client.post( -# "http://localhost:8000/v1/projects/dummy-fi/suggest-batch", json=data -# ) -# assert req.status_code == 400 -# assert req.json()["detail"] == "too many items - 'documents'" +def test_openapi_suggest_batch_too_many_documents(app_client): + data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}] * 33} + req = app_client.post( + "http://localhost:8000/v1/projects/dummy-fi/suggest-batch", json=data + ) + assert req.status_code == 400 + assert req.json()["detail"] == "too many items - 'documents'" def test_openapi_learn(app_client): From 89f9c3ca0546c0a76546c1c06d0077112c36b692 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:44:36 +0300 Subject: [PATCH 18/28] Use port 5000 by default like with Connexion 2 --- annif/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annif/cli.py b/annif/cli.py index 833cdbfb0..979c5bfb1 100644 --- a/annif/cli.py +++ b/annif/cli.py @@ -439,7 +439,7 @@ def run_eval( @cli.command("run") -@click.option("--port", type=int) +@click.option("--port", type=int, default=5000) @click.option("--log-level") @click_log.simple_verbosity_option(logger, default="ERROR") def run_app(**kwargs): From 7c91ebacb0d914c7b484937c3eb29d4f7e211bf7 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:52:45 +0300 Subject: [PATCH 19/28] Remove --env-file and --app options --- annif/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/annif/cli.py b/annif/cli.py index 979c5bfb1..63a93c795 100644 --- a/annif/cli.py +++ b/annif/cli.py @@ -30,6 +30,7 @@ create_app=create_app, add_default_commands=False, add_version_option=False ) cli = click.version_option(message="%(version)s")(cli) +cli.params = [opt for opt in cli.params if opt.name not in ("env_file", "app")] @cli.command("list-projects") From c57cc14b2482d64ee51434ebfc9bd16c4a4c43e7 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:05:11 +0300 Subject: [PATCH 20/28] Allow only patch level updates on Connexion versions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d47986622..be4d87726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.9,<3.12" -connexion = {version = ">=3.0.5", extras = ["flask", "uvicorn", "swagger-ui"]} +connexion = { version = "~3.0.5", extras = ["flask", "uvicorn", "swagger-ui"] } click = "8.1.*" click-log = "0.4.*" joblib = "1.3.*" From 2f7aaa594241bbcea9443845b76f2b9e19c91287 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:13:52 +0300 Subject: [PATCH 21/28] Fix hints for return types --- annif/rest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/annif/rest.py b/annif/rest.py index 243348f6b..c7f457687 100644 --- a/annif/rest.py +++ b/annif/rest.py @@ -14,8 +14,6 @@ from annif.project import Access if TYPE_CHECKING: - from datetime import datetime - from connexion.lifecycle import ConnexionResponse from annif.corpus.subject import SubjectIndex @@ -43,7 +41,7 @@ def server_error( ) -def show_info() -> dict[str, str]: +def show_info() -> tuple: """return version of annif and a title for the api according to OpenAPI spec""" result = {"title": "Annif REST API", "version": importlib.metadata.version("annif")} @@ -60,7 +58,7 @@ def language_not_supported_error(lang: str) -> ConnexionResponse: ) -def list_projects() -> dict[str, list[dict[str, str | dict | bool | datetime | None]]]: +def list_projects() -> tuple: """return a dict with projects formatted according to OpenAPI spec""" result = { From ab33375901d83483e348d92423b4432495b8fecd Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:28:35 +0300 Subject: [PATCH 22/28] Add test for empty RequestBody to suggest request --- tests/test_openapi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 76f33695f..d7f347809 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -83,6 +83,15 @@ def test_openapi_suggest_novocab(app_client): assert req.status_code == 503 +def test_openapi_suggest_emptybody(app_client): + data = {} + req = app_client.post( + "http://localhost:8000/v1/projects/dummy-fi/suggest", data=data + ) + assert req.status_code == 400 + assert req.json()["detail"] == "RequestBody is required" + + def test_openapi_suggest_batch(app_client): data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}] * 32} req = app_client.post( From 60e95f6946db3a9f219916b2f3810033349f3cfb Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:42:42 +0300 Subject: [PATCH 23/28] Omit line from CodeCov covererage report because this case is tested --- annif/openapi/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annif/openapi/validation.py b/annif/openapi/validation.py index 5cf4da392..ee541e344 100644 --- a/annif/openapi/validation.py +++ b/annif/openapi/validation.py @@ -22,7 +22,7 @@ def __init__(self, *args, **kwargs) -> None: def _validate(self, body: Any) -> dict | None: if not self._nullable and body is None: - raise BadRequestProblem("Request body must not be empty") + raise BadRequestProblem("Request body must not be empty") # noqa try: return self._validator.validate(body) except ValidationError as exception: From 8a2d0d571a4b8c875142b37dc7b5a4a2b3ea645d Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:53:27 +0300 Subject: [PATCH 24/28] Use the right annotation to exlude line from codecov report --- annif/openapi/validation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/annif/openapi/validation.py b/annif/openapi/validation.py index ee541e344..f28086050 100644 --- a/annif/openapi/validation.py +++ b/annif/openapi/validation.py @@ -22,7 +22,9 @@ def __init__(self, *args, **kwargs) -> None: def _validate(self, body: Any) -> dict | None: if not self._nullable and body is None: - raise BadRequestProblem("Request body must not be empty") # noqa + raise BadRequestProblem( + "Request body must not be empty" + ) # pragma: no cover try: return self._validator.validate(body) except ValidationError as exception: From fc45493546152ac8bf2cda23610b41125f4b58a6 Mon Sep 17 00:00:00 2001 From: Juho Inkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:59:35 +0300 Subject: [PATCH 25/28] Use uvicorn workers in docker-compose setup --- docker-compose.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index aed2211ec..efe40d0c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,17 @@ services: volumes: - ${ANNIF_PROJECTS}:/annif-projects user: ${MY_UID}:${MY_GID} - command: ["gunicorn", "annif:create_app()", "--bind", "0.0.0.0:8000", "--timeout", "600"] + command: + [ + "gunicorn", + "-k", + "uvicorn.workers.UvicornWorker", + "annif:create_app()", + "--bind", + "0.0.0.0:8000", + "--timeout", + "600" + ] nginx: image: nginx From ad464e439e445b2402e068a732a17398449e4904 Mon Sep 17 00:00:00 2001 From: juhoinkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:01:43 +0300 Subject: [PATCH 26/28] Set env in Dockerfile to make Gunicorn use Uvicorn workers This also reverts the previous commit fc45493546152ac8bf2cda23610b41125f4b58a6. Now uvicorn workers are used always when running with Docker --- Dockerfile | 2 ++ docker-compose.yml | 12 +----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52198a69f..6d319d31f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,4 +54,6 @@ RUN groupadd -g 998 annif_user && \ chown -R annif_user:annif_user /annif-projects /Annif/tests/data USER annif_user +ENV GUNICORN_CMD_ARGS="--worker-class uvicorn.workers.UvicornWorker" + CMD annif diff --git a/docker-compose.yml b/docker-compose.yml index efe40d0c1..aed2211ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,17 +7,7 @@ services: volumes: - ${ANNIF_PROJECTS}:/annif-projects user: ${MY_UID}:${MY_GID} - command: - [ - "gunicorn", - "-k", - "uvicorn.workers.UvicornWorker", - "annif:create_app()", - "--bind", - "0.0.0.0:8000", - "--timeout", - "600" - ] + command: ["gunicorn", "annif:create_app()", "--bind", "0.0.0.0:8000", "--timeout", "600"] nginx: image: nginx From 5f022281232e415846efda9a9242f53e15d9cf96 Mon Sep 17 00:00:00 2001 From: juhoinkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:22:24 +0300 Subject: [PATCH 27/28] Adapt fixture from merged another PR to this PR --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fae4ab2aa..7d7a851ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,8 +41,8 @@ def app_with_initialize(): @pytest.fixture(scope="module") @unittest.mock.patch.dict(os.environ, {"ANNIF_PROJECTS_INIT": ".*-fi"}) def app_with_initialize_fi_projects(): - app = annif.create_app(config_name="annif.default_config.TestingInitializeConfig") - return app + cxapp = annif.create_app(config_name="annif.default_config.TestingInitializeConfig") + return cxapp.app @pytest.fixture From e1e5d5a591cc143527b9d09886fbd4e9f3801cad Mon Sep 17 00:00:00 2001 From: juhoinkinen <34240031+juhoinkinen@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:22:55 +0300 Subject: [PATCH 28/28] Remove useless test and condition in validator The test was added in ab33375901d83483e348d92423b4432495b8fecd The condition is checked before this custom validation --- annif/openapi/validation.py | 8 ++------ tests/test_openapi.py | 9 --------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/annif/openapi/validation.py b/annif/openapi/validation.py index f28086050..2fce37732 100644 --- a/annif/openapi/validation.py +++ b/annif/openapi/validation.py @@ -15,20 +15,16 @@ class CustomRequestBodyValidator(JSONRequestBodyValidator): """Custom request body validator that overrides the default error message for the - 'maxItems' validator for the 'documents' property.""" + 'maxItems' validator for the 'documents' property to prevent logging request body + with the contents of all documents.""" def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def _validate(self, body: Any) -> dict | None: - if not self._nullable and body is None: - raise BadRequestProblem( - "Request body must not be empty" - ) # pragma: no cover try: return self._validator.validate(body) except ValidationError as exception: - # Prevent logging request body with contents of all documents if exception.validator == "maxItems" and list(exception.schema_path) == [ "properties", "documents", diff --git a/tests/test_openapi.py b/tests/test_openapi.py index d7f347809..76f33695f 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -83,15 +83,6 @@ def test_openapi_suggest_novocab(app_client): assert req.status_code == 503 -def test_openapi_suggest_emptybody(app_client): - data = {} - req = app_client.post( - "http://localhost:8000/v1/projects/dummy-fi/suggest", data=data - ) - assert req.status_code == 400 - assert req.json()["detail"] == "RequestBody is required" - - def test_openapi_suggest_batch(app_client): data = {"documents": [{"text": "A quick brown fox jumped over the lazy dog."}] * 32} req = app_client.post(