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

Upgrade to Connexion3 #702

Merged
merged 34 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4b4e621
Upgrade to Connexion 3 alpha-6
juhoinkinen Apr 28, 2023
57fcaa9
Make application/x-www-form-urlencoded request with axios
juhoinkinen May 9, 2023
f046c1f
Fix OpenAPI spec for /learn endpoint
juhoinkinen May 9, 2023
6d0a68f
Set content-types in response headers in rest methods
juhoinkinen May 3, 2023
c315633
Adapt tests for rest returning also status codes & content types
juhoinkinen May 10, 2023
e417e02
Adapt tests and fixtures for using Connexion app
juhoinkinen May 10, 2023
9cd6a60
Upgrade to Connexion 3.0.* (from alpha); remove direct Flask dependency
juhoinkinen Nov 15, 2023
5d7ec95
Merge branch 'main' into upgrade-to-connexion3
juhoinkinen Nov 15, 2023
067983b
Drop using flask-cors (TODO use CORSMiddleware)
juhoinkinen Nov 15, 2023
d485a3f
Drop using flask-cors (TODO use CORSMiddleware)
juhoinkinen Nov 15, 2023
9ca8d9a
Merge branch 'main' into upgrade-to-connexion3
juhoinkinen Dec 20, 2023
3902748
Add annif run CLI command for starting uvicorn dev server
juhoinkinen Dec 21, 2023
cb39aa0
Merge branch 'upgrade-to-connexion3' of github.com:NatLibFi/Annif int…
juhoinkinen Dec 21, 2023
aee036f
Require connexion version >= 3.0.5
juhoinkinen Jan 9, 2024
5aa1cf3
Merge branch 'main' into upgrade-to-connexion3
juhoinkinen Jan 9, 2024
a94f707
Merge branch 'main' into upgrade-to-connexion3
juhoinkinen Apr 4, 2024
beac55c
Re-enable CORS
juhoinkinen Apr 4, 2024
556a29a
Exclude fuzzy cases where path parameters contain newline "%0A"
juhoinkinen Apr 5, 2024
f4c28dc
Adapt tests for removed "annif routes" and customized "annif run" com…
juhoinkinen Apr 5, 2024
a0a7246
Fix slow fuzzy test by making it use cxapp and asgi test calls
juhoinkinen Apr 5, 2024
f12ca5e
Test with mocking that run command tries to start up server
juhoinkinen Apr 5, 2024
3703423
Update CustomRequestBodyValidator for Connexion 3 and re-enable its test
juhoinkinen Apr 8, 2024
89f9c3c
Use port 5000 by default like with Connexion 2
juhoinkinen Apr 11, 2024
7c91eba
Remove --env-file and --app options
juhoinkinen Apr 12, 2024
c57cc14
Allow only patch level updates on Connexion versions
juhoinkinen Apr 12, 2024
2f7aaa5
Fix hints for return types
juhoinkinen Apr 12, 2024
ab33375
Add test for empty RequestBody to suggest request
juhoinkinen Apr 12, 2024
60e95f6
Omit line from CodeCov covererage report because this case is tested
juhoinkinen Apr 12, 2024
8a2d0d5
Use the right annotation to exlude line from codecov report
juhoinkinen Apr 12, 2024
fc45493
Use uvicorn workers in docker-compose setup
juhoinkinen Apr 12, 2024
ad464e4
Set env in Dockerfile to make Gunicorn use Uvicorn workers
juhoinkinen Apr 17, 2024
9cc9e64
Merge branch 'main' into upgrade-to-connexion3
juhoinkinen Apr 17, 2024
5f02228
Adapt fixture from merged another PR to this PR
juhoinkinen Apr 17, 2024
e1e5d5a
Remove useless test and condition in validator
juhoinkinen Apr 19, 2024
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
18 changes: 10 additions & 8 deletions annif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,21 @@ def create_app(config_name: str | None = None) -> Flask:
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)
Expand All @@ -64,8 +66,8 @@ def create_app(config_name: str | None = None) -> Flask:

cxapp.app.register_blueprint(bp)

# return the Flask app
return cxapp.app
# return the Connexion app
return cxapp


def _get_config_name(config_name: str | None) -> str:
Expand Down
4 changes: 3 additions & 1 deletion annif/openapi/annif.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ paths:
responses:
"204":
description: successful operation
content: {}
content:
application/json:
{}
"404":
$ref: '#/components/responses/NotFound'
"503":
Expand Down
14 changes: 8 additions & 6 deletions annif/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def server_error(
def show_info() -> dict[str, str]:
"""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: str) -> ConnexionResponse:
Expand All @@ -61,12 +62,13 @@ def language_not_supported_error(lang: str) -> ConnexionResponse:
def list_projects() -> dict[str, list[dict[str, str | dict | bool | datetime | None]]]:
"""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(
Expand All @@ -78,7 +80,7 @@ def show_project(
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(
Expand Down Expand Up @@ -123,7 +125,7 @@ def suggest(

if _is_error(result):
return result
return result[0]
return result[0], 200, {"Content-Type": "application/json"}


def suggest_batch(
Expand All @@ -141,7 +143,7 @@ def suggest_batch(
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(
Expand Down Expand Up @@ -213,4 +215,4 @@ def learn(
except AnnifException as err:
return server_error(err)

return None, 204
return None, 204, {"Content-Type": "application/json"}
2 changes: 1 addition & 1 deletion annif/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ <h2 class="mt-4" id="suggestions">Suggested subjects</h2>\
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;
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ classifiers=[
[tool.poetry.dependencies]
python = ">=3.8,<3.12"

connexion = {version = "2.14.2", extras = ["swagger-ui"]}
flask = "2.2.*"
connexion = {version = "3.0.*", extras = ["flask","uvicorn", "swagger-ui"]}
flask-cors = "4.0.*"
click = "8.1.*"
click-log = "0.4.*"
Expand Down
21 changes: 13 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
26 changes: 13 additions & 13 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

@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,))


Expand All @@ -31,13 +31,13 @@
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):
Expand All @@ -51,7 +51,7 @@
"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):
Expand All @@ -76,18 +76,18 @@
"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
Fixed Show fixed Hide fixed
# )
# assert req.status_code == 400
# assert req.json()["detail"] == "too many items - 'documents'"


def test_openapi_learn(app_client):
Expand Down
16 changes: 8 additions & 8 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -301,23 +301,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"
Loading
Loading