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

Optimize CLI startup time #696

Merged
merged 7 commits into from
May 11, 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
43 changes: 30 additions & 13 deletions annif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,38 @@
import os
import os.path

import connexion
from flask_cors import CORS

from annif.openapi.validation import CustomRequestBodyValidator

logging.basicConfig()
logger = logging.getLogger("annif")
logger.setLevel(level=logging.INFO)

import annif.backend # noqa


def create_flask_app(config_name=None):
"""Create a Flask app to be used by the CLI."""
from flask import Flask

app = Flask(__name__)
config_name = _get_config_name(config_name)
logger.debug(f"creating flask app with configuration {config_name}")
app.config.from_object(config_name)
app.config.from_envvar("ANNIF_SETTINGS", silent=True)
return app


def create_app(config_name=None):
"""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 annif.openapi.validation import CustomRequestBodyValidator

specdir = os.path.join(os.path.dirname(__file__), "openapi")
cxapp = connexion.App(__name__, specification_dir=specdir)
if config_name is None:
config_name = os.environ.get("ANNIF_CONFIG")
if config_name is None:
if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
config_name = "annif.default_config.Config"
else:
config_name = "annif.default_config.ProductionConfig"
logger.debug("creating app with configuration %s", config_name)
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)

Expand All @@ -52,3 +58,14 @@ def create_app(config_name=None):

# return the Flask app
return cxapp.app


def _get_config_name(config_name):
if config_name is None:
config_name = os.environ.get("ANNIF_CONFIG")
if config_name is None:
if os.environ.get("FLASK_RUN_FROM_CLI") == "true": # pragma: no cover
config_name = "annif.default_config.Config"
else:
config_name = "annif.default_config.ProductionConfig" # pragma: no cover
return config_name
9 changes: 8 additions & 1 deletion annif/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
logger = annif.logger
click_log.basic_config(logger)

cli = FlaskGroup(create_app=annif.create_app, add_version_option=False)

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)
cli = click.version_option(message="%(version)s")(cli)


Expand Down
6 changes: 5 additions & 1 deletion annif/corpus/skos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os.path
import shutil

import joblib
import rdflib
import rdflib.util
from rdflib.namespace import OWL, RDF, RDFS, SKOS
Expand All @@ -17,6 +16,7 @@
def serialize_subjects_to_skos(subjects, path):
"""Create a SKOS representation of the given subjects and serialize it
into a SKOS/Turtle file with the given path name."""
import joblib

graph = rdflib.Graph()
graph.namespace_manager.bind("skos", SKOS)
Expand Down Expand Up @@ -54,6 +54,8 @@ class SubjectFileSKOS(SubjectCorpus):
def __init__(self, path):
self.path = path
if path.endswith(".dump.gz"):
import joblib

self.graph = joblib.load(path)
else:
self.graph = rdflib.Graph()
Expand Down Expand Up @@ -132,6 +134,8 @@ def save_skos(self, path):
# need to serialize into Turtle
self.graph.serialize(destination=path, format="turtle")
# also dump the graph in joblib format which is faster to load
import joblib

annif.util.atomic_save(
self.graph,
*os.path.split(path.replace(".ttl", ".dump.gz")),
Expand Down
4 changes: 2 additions & 2 deletions annif/corpus/subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import csv
import os.path

import numpy as np

import annif
import annif.util

Expand Down Expand Up @@ -273,6 +271,8 @@ def as_vector(self, size=None, destination=None):
None), otherwise create and return a new one of the given size."""

if destination is None:
import numpy as np

assert size is not None and size > 0
destination = np.zeros(size, dtype=bool)

Expand Down
21 changes: 21 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,27 @@ def test_version_option():
assert result.output.strip() == version.strip()


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\/\<path:filename\>", 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\/<path:filename>", result)
assert re.search(r"app.home\s+GET\s+\/", result)


def test_completion_script_generation():
result = runner.invoke(annif.cli.cli, ["completion", "--bash"])
assert not result.exception
Expand Down