Commit 123ac1df authored by Konstantin Schulz's avatar Konstantin Schulz

API documentation is now available at ... /mc/api/v1.0/ui/

parent 90b6557c
Pipeline #11569 passed with stages
in 7 minutes and 57 seconds
...@@ -13,7 +13,6 @@ services: ...@@ -13,7 +13,6 @@ services:
- IS_THIS_A_DOCKER_CONTAINER=Yes - IS_THIS_A_DOCKER_CONTAINER=Yes
- PYTHONPATH=/home/mc - PYTHONPATH=/home/mc
ports: ports:
- "8122:22"
- "6555:6555" - "6555:6555"
restart: always restart: always
stdin_open: true stdin_open: true
...@@ -21,6 +20,8 @@ services: ...@@ -21,6 +20,8 @@ services:
image: postgres image: postgres
environment: environment:
- POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_HOST_AUTH_METHOD=trust
# ports:
# - "5432:5432"
restart: always restart: always
volumes: volumes:
- db-data:/var/lib/postgresql/data - db-data:/var/lib/postgresql/data
...@@ -47,7 +48,6 @@ services: ...@@ -47,7 +48,6 @@ services:
- IS_THIS_A_DOCKER_CONTAINER=Yes - IS_THIS_A_DOCKER_CONTAINER=Yes
- PYTHONPATH=/home/mc - PYTHONPATH=/home/mc
ports: ports:
- "8022:22"
- "5000:5000" - "5000:5000"
restart: always restart: always
stdin_open: true stdin_open: true
......
...@@ -6,6 +6,8 @@ cover_pylib = False ...@@ -6,6 +6,8 @@ cover_pylib = False
omit = omit =
*/site-packages/* */site-packages/*
*/migrations/* */migrations/*
# cannot run tests for files that are generated and updated automatically
*/models_auto.py
parallel = True parallel = True
[report] [report]
......
...@@ -34,13 +34,14 @@ Or combine both commands in one line: `pip list -o --format=freeze | grep -v '^\ ...@@ -34,13 +34,14 @@ Or combine both commands in one line: `pip list -o --format=freeze | grep -v '^\
---------------------------------------------------------------- ----------------------------------------------------------------
# Database # Database
To autogenerate a new migration script, start the Docker container with the database and run: `flask db migrate`. To autogenerate a new migration script:
1. Start the Docker container with the database: `docker-compose run -p 5432:5432 -d db`
To migrate the database to a newer version manually, run: `flask db upgrade` 2. Create a new migration: `flask db migrate`.
3. Perform a migration...
To migrate the database to a newer version manually, run: `flask db downgrade` - ... to a newer version: `flask db upgrade`.
- ... to an older version: `flask db downgrade`.
If it does nothing or fails, make sure that the environment variable FLASK_APP is set correctly (see https://flask.palletsprojects.com/en/1.1.x/cli/). - If it does nothing or fails, make sure that the environment variable FLASK_APP is set correctly (see https://flask.palletsprojects.com/en/1.1.x/cli/): `export FLASK_APP=app.py`
5. To finish the process, shut down the database container: `docker-compose down`
---------------------------------------------------------------- ----------------------------------------------------------------
......
from flask import Flask from flask import Flask
from mcserver import get_app, get_cfg from mcserver import get_app, get_cfg
app: Flask = get_app() app: Flask = get_app()
......
...@@ -7,7 +7,7 @@ from mcserver import Config ...@@ -7,7 +7,7 @@ from mcserver import Config
from mcserver.app import init_app_common, init_logging from mcserver.app import init_app_common, init_logging
def create_csm_app(cfg: Type[Config] = Config): def create_csm_app(cfg: Type[Config] = Config) -> Flask:
"""Creates a new Flask app that represents a Corpus Storage Manager.""" """Creates a new Flask app that represents a Corpus Storage Manager."""
Config.CORPUS_STORAGE_MANAGER = CorpusStorageManager(Config.GRAPH_DATABASE_DIR) Config.CORPUS_STORAGE_MANAGER = CorpusStorageManager(Config.GRAPH_DATABASE_DIR)
......
...@@ -4,9 +4,10 @@ It is a server-side backend for retrieving Latin texts and ...@@ -4,9 +4,10 @@ It is a server-side backend for retrieving Latin texts and
generating language exercises for them.""" generating language exercises for them."""
import sys import sys
from typing import Type from typing import Type
from flask import Flask from flask import Flask
from mcserver.app import create_app
from mcserver.config import Config, ProductionConfig, TestingConfig, DevelopmentConfig from mcserver.config import Config, ProductionConfig, TestingConfig, DevelopmentConfig
from mcserver.app import create_app
def get_app() -> Flask: def get_app() -> Flask:
......
"""The main module for the application. It contains the application factory and provides access to the database.""" """The main module for the application. It contains the application factory and provides access to the database."""
import logging import logging
import os
import sys import sys
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from threading import Thread from threading import Thread
from time import strftime from time import strftime
from typing import Type from typing import Type
import connexion
import flask import flask
from connexion import FlaskApp
from flask import Flask, got_request_exception, request, Response, send_from_directory from flask import Flask, got_request_exception, request, Response, send_from_directory
from flask_cors import CORS from flask_cors import CORS
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from open_alchemy import init_yaml
from mcserver.config import Config from mcserver.config import Config
db: SQLAlchemy = SQLAlchemy() # session_options={"autocommit": True} db: SQLAlchemy = SQLAlchemy() # session_options={"autocommit": True}
migrate: Migrate = Migrate(directory=Config.MIGRATIONS_DIRECTORY) migrate: Migrate = Migrate(directory=Config.MIGRATIONS_DIRECTORY)
# do this _BEFORE_ you add any APIs to your application
init_yaml(Config.API_SPEC_FILE_PATH, base=db.Model,
models_filename=os.path.join(Config.MC_SERVER_DIRECTORY, "models_auto.py"))
def apply_event_handlers(app: FlaskApp):
"""Applies event handlers to a given Flask application, such as logging after requests or teardown logic."""
@app.app.after_request
def after_request(response: Response) -> Response:
""" Logs metadata for every request. """
timestamp = strftime('[%Y-%m-%d %H:%M:%S]')
app.app.logger.info('%s %s %s %s %s %s', timestamp, request.remote_addr, request.method, request.scheme,
request.full_path, response.status)
return response
@app.route(Config.SERVER_URI_FAVICON)
def get_favicon():
"""Sends the favicon to browsers, which is used, e.g., in the tabs as a symbol for our application."""
mime_type: str = 'image/vnd.microsoft.icon'
return send_from_directory(Config.ASSETS_DIRECTORY, Config.FAVICON_FILE_NAME, mimetype=mime_type)
app.app.teardown_appcontext(shutdown_session)
def create_app(cfg: Type[Config] = Config) -> Flask: def create_app(cfg: Type[Config] = Config) -> Flask:
...@@ -26,7 +52,7 @@ def create_app(cfg: Type[Config] = Config) -> Flask: ...@@ -26,7 +52,7 @@ def create_app(cfg: Type[Config] = Config) -> Flask:
# use local postgres database for migrations # use local postgres database for migrations
if len(sys.argv) > 2 and sys.argv[2] == Config.FLASK_MIGRATE: if len(sys.argv) > 2 and sys.argv[2] == Config.FLASK_MIGRATE:
cfg.SQLALCHEMY_DATABASE_URI = Config.DATABASE_URL_LOCAL cfg.SQLALCHEMY_DATABASE_URI = Config.DATABASE_URL_LOCAL
app = init_app_common(cfg=cfg) app: Flask = init_app_common(cfg=cfg)
from mcserver.app.services import bp as services_bp from mcserver.app.services import bp as services_bp
app.register_blueprint(services_bp) app.register_blueprint(services_bp)
from mcserver.app.api import bp as api_bp from mcserver.app.api import bp as api_bp
...@@ -51,27 +77,12 @@ def full_init(app: Flask, is_csm: bool) -> None: ...@@ -51,27 +77,12 @@ def full_init(app: Flask, is_csm: bool) -> None:
def init_app_common(cfg: Type[Config] = Config, is_csm: bool = False) -> Flask: def init_app_common(cfg: Type[Config] = Config, is_csm: bool = False) -> Flask:
""" Initializes common Flask parts, e.g. CORS, configuration, database, migrations and custom corpora.""" """ Initializes common Flask parts, e.g. CORS, configuration, database, migrations and custom corpora."""
app = Flask(__name__) connexion_app: FlaskApp = connexion.FlaskApp(
__name__, port=(cfg.CORPUS_STORAGE_MANAGER_PORT if is_csm else cfg.HOST_PORT),
@app.after_request specification_dir=Config.MC_SERVER_DIRECTORY)
def after_request(response: Response) -> Response: connexion_app.add_api(Config.API_SPEC_FILE_PATH, arguments={'title': 'Machina Callida Backend REST API'})
""" Logs metadata for every request. """ apply_event_handlers(connexion_app)
timestamp = strftime('[%Y-%m-%d %H:%M:%S]') app: Flask = connexion_app.app
app.logger.info('%s %s %s %s %s %s', timestamp, request.remote_addr, request.method, request.scheme,
request.full_path, response.status)
return response
@app.route(Config.SERVER_URI_FAVICON)
def get_favicon():
"""Sends the favicon to browsers, which is used, e.g., in the tabs as a symbol for our application."""
mime_type: str = 'image/vnd.microsoft.icon'
return send_from_directory(Config.ASSETS_DIRECTORY, Config.FAVICON_FILE_NAME, mimetype=mime_type)
@app.teardown_appcontext
def shutdown_session(exception=None):
""" Shuts down the session when the application exits. (maybe also after every request ???) """
db.session.remove()
# allow CORS requests for all API routes # allow CORS requests for all API routes
CORS(app) # , resources=r"/*" CORS(app) # , resources=r"/*"
app.config.from_object(cfg) app.config.from_object(cfg)
...@@ -120,6 +131,12 @@ def start_updater(app: Flask) -> Thread: ...@@ -120,6 +131,12 @@ def start_updater(app: Flask) -> Thread:
return t return t
def shutdown_session(exception=None):
""" Shuts down the session when the application exits. (maybe also after every request ???) """
db.session.remove()
# import the models so we can access them from other parts of the app using imports from "app.models"; # import the models so we can access them from other parts of the app using imports from "app.models";
# this has to be at the bottom of the file # this has to be at the bottom of the file
from mcserver.app import models from mcserver.app import models
from mcserver.app import api
"""The API blueprint. Register it on the main application to enable the REST API for text retrieval.""" """The API blueprint. Register it on the main application to enable the REST API for text retrieval."""
from flask import Blueprint from flask import Blueprint
from flask_restful import Api from flask_restful import Api
from mcserver import Config from mcserver import Config
bp = Blueprint("api", __name__) bp = Blueprint("api", __name__)
api = Api(bp) api = Api(bp)
from mcserver.app.api.corpusAPI import CorpusAPI from . import corpusAPI, corpusListAPI, exerciseAPI
from mcserver.app.api.corpusListAPI import CorpusListAPI
from mcserver.app.api.exerciseAPI import ExerciseAPI
from mcserver.app.api.exerciseListAPI import ExerciseListAPI from mcserver.app.api.exerciseListAPI import ExerciseListAPI
from mcserver.app.api.fileAPI import FileAPI from mcserver.app.api.fileAPI import FileAPI
from mcserver.app.api.frequencyAPI import FrequencyAPI from mcserver.app.api.frequencyAPI import FrequencyAPI
...@@ -22,9 +19,6 @@ from mcserver.app.api.validReffAPI import ValidReffAPI ...@@ -22,9 +19,6 @@ from mcserver.app.api.validReffAPI import ValidReffAPI
from mcserver.app.api.vectorNetworkAPI import VectorNetworkAPI from mcserver.app.api.vectorNetworkAPI import VectorNetworkAPI
from mcserver.app.api.vocabularyAPI import VocabularyAPI from mcserver.app.api.vocabularyAPI import VocabularyAPI
api.add_resource(CorpusListAPI, Config.SERVER_URI_CORPORA, endpoint="corpora")
api.add_resource(CorpusAPI, Config.SERVER_URI_CORPORA + "/<int:cid>", endpoint="corpus")
api.add_resource(ExerciseAPI, Config.SERVER_URI_EXERCISE, endpoint="exercise")
api.add_resource(ExerciseListAPI, Config.SERVER_URI_EXERCISE_LIST, endpoint="exerciseList") api.add_resource(ExerciseListAPI, Config.SERVER_URI_EXERCISE_LIST, endpoint="exerciseList")
api.add_resource(FileAPI, Config.SERVER_URI_FILE, endpoint="file") api.add_resource(FileAPI, Config.SERVER_URI_FILE, endpoint="file")
api.add_resource(FrequencyAPI, Config.SERVER_URI_FREQUENCY, endpoint="frequency") api.add_resource(FrequencyAPI, Config.SERVER_URI_FREQUENCY, endpoint="frequency")
......
"""The corpus API. Add it to your REST API to provide users with metadata about specific texts.""" """The corpus API. Add it to your REST API to provide users with metadata about specific texts."""
from flask_restful import Resource, abort, marshal from typing import Union
from flask_restful.reqparse import RequestParser
import connexion
from connexion.lifecycle import ConnexionResponse
from flask import Response
from mcserver import Config
from mcserver.app import db from mcserver.app import db
from mcserver.app.models import Corpus, corpus_fields
from mcserver.app.services import NetworkService from mcserver.app.services import NetworkService
from mcserver.models_auto import Corpus
def delete(cid: int) -> Union[Response, ConnexionResponse]:
"""The DELETE method for the corpus REST API. It deletes metadata for a specific text."""
corpus: Corpus = db.session.query(Corpus).filter_by(cid=cid).first()
if corpus is None:
return connexion.problem(404, Config.ERROR_TITLE_NOT_FOUND, Config.ERROR_MESSAGE_CORPUS_NOT_FOUND)
db.session.delete(corpus)
db.session.commit()
return NetworkService.make_json_response(True)
def get(cid: int) -> Union[Response, ConnexionResponse]:
"""The GET method for the corpus REST API. It provides metadata for a specific text."""
corpus: Corpus = db.session.query(Corpus).filter_by(cid=cid).first()
if corpus is None:
return connexion.problem(404, Config.ERROR_TITLE_NOT_FOUND, Config.ERROR_MESSAGE_CORPUS_NOT_FOUND)
return NetworkService.make_json_response(corpus.to_dict())
class CorpusAPI(Resource): def patch(cid: int, **kwargs) -> Union[Response, ConnexionResponse]:
"""The corpus API resource. It enables some of the CRUD operations for metadata about specific texts.""" """The PUT method for the corpus REST API. It provides updates metadata for a specific text."""
corpus: Corpus = db.session.query(Corpus).filter_by(cid=cid).first()
def __init__(self): if corpus is None:
"""Initialize possible arguments for calls to the corpus REST API.""" return connexion.problem(404, Config.ERROR_TITLE_NOT_FOUND, Config.ERROR_MESSAGE_CORPUS_NOT_FOUND)
self.reqparse: RequestParser = NetworkService.base_request_parser.copy() for k, v in kwargs.items():
self.reqparse.add_argument("title", type=str, required=False, help="No title provided") if v is not None:
self.reqparse.add_argument("author", type=str, required=False, help="No author provided") setattr(corpus, k, v)
self.reqparse.add_argument("source_urn", type=str, required=False, help="No source URN provided") db.session.commit()
super(CorpusAPI, self).__init__() return NetworkService.make_json_response(corpus.to_dict())
def get(self, cid):
"""The GET method for the corpus REST API. It provides metadata for a specific text."""
corpus: Corpus = Corpus.query.filter_by(cid=cid).first()
if corpus is None:
abort(404)
return {"corpus": marshal(corpus, corpus_fields)}
def put(self, cid):
"""The PUT method for the corpus REST API. It provides updates metadata for a specific text."""
corpus: Corpus = Corpus.query.filter_by(cid=cid).first()
if corpus is None:
abort(404)
args = self.reqparse.parse_args()
for k, v in args.items():
if v is not None:
setattr(corpus, k, v)
db.session.commit()
return {"corpus": marshal(corpus, corpus_fields)}
def delete(self, cid):
"""The DELETE method for the corpus REST API. It deletes metadata for a specific text."""
corpus: Corpus = Corpus.query.filter_by(cid=cid).first()
if corpus is None:
abort(404)
db.session.delete(corpus)
db.session.commit()
return {"result": True}
"""The corpus list API. Add it to your REST API to provide users with a list of metadata for available texts.""" """The corpus list API. Add it to your REST API to provide users with a list of metadata for available texts."""
from datetime import datetime from datetime import datetime
from flask import jsonify from connexion.lifecycle import ConnexionResponse
from flask_restful import Resource, marshal from flask import Response
from flask_restful.reqparse import RequestParser
from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.exc import OperationalError, InvalidRequestError
from typing import List, Union
from mcserver.app import db from mcserver.app import db
from mcserver.app.models import UpdateInfo, ResourceType, Corpus, corpus_fields from mcserver.app.models import ResourceType
from mcserver.app.services import CorpusService, NetworkService from mcserver.app.services import NetworkService
from mcserver.models_auto import Corpus, UpdateInfo
class CorpusListAPI(Resource):
"""The corpus list API resource. It enables some of the CRUD operations for a list of metadata about all texts."""
def __init__(self):
"""Initialize possible arguments for calls to the corpus list REST API."""
self.reqparse: RequestParser = NetworkService.base_request_parser.copy()
self.reqparse.add_argument("last_update_time", type=int, required=True,
help="No milliseconds time for last update provided")
super(CorpusListAPI, self).__init__()
def get(self): def get(last_update_time: int) -> Union[Response, ConnexionResponse]:
"""The GET method for the corpus list REST API. It provides metadata for all available texts.""" """The GET method for the corpus list REST API. It provides metadata for all available texts."""
args = self.reqparse.parse_args() ui_cts: UpdateInfo
last_update: int = args["last_update_time"] try:
last_update_time: datetime = datetime.fromtimestamp(last_update / 1000.0) ui_cts = db.session.query(UpdateInfo).filter_by(resource_type=ResourceType.cts_data.name).first()
ui_cts: UpdateInfo except (InvalidRequestError, OperationalError):
try: db.session.rollback()
ui_cts = UpdateInfo.query.filter_by(resource_type=ResourceType.cts_data.name).first() return NetworkService.make_json_response(None)
except (InvalidRequestError, OperationalError): if ui_cts.last_modified_time >= last_update_time / 1000:
db.session.rollback() corpora: List[Corpus] = db.session.query(Corpus).all()
return None return NetworkService.make_json_response([x.to_dict() for x in corpora])
if ui_cts.last_modified_time >= last_update_time: return NetworkService.make_json_response(None)
CorpusService.existing_corpora = Corpus.query.all()
return jsonify({"corpora": [marshal(corpus, corpus_fields) for corpus in CorpusService.existing_corpora]})
return None
import uuid import uuid
from collections import OrderedDict
from datetime import datetime from datetime import datetime
import connexion
import rapidjson as json import rapidjson as json
from typing import List, Dict from typing import List, Dict, Union
import requests import requests
from flask_restful import Resource, marshal, abort from connexion.lifecycle import ConnexionResponse
from flask_restful.reqparse import RequestParser from flask import Response
from mcserver.app import db from mcserver.app import db
from mcserver.app.models import ExerciseType, Solution, ExerciseData, Exercise, exercise_fields, AnnisResponse, \ from mcserver.app.models import ExerciseType, Solution, ExerciseData, AnnisResponse, Phenomenon, TextComplexity, \
Phenomenon, TextComplexity, TextComplexityMeasure, UpdateInfo, ResourceType TextComplexityMeasure, ResourceType, ExerciseMC
from mcserver.app.services import AnnotationService, CorpusService, NetworkService, TextComplexityService from mcserver.app.services import AnnotationService, CorpusService, NetworkService, TextComplexityService
from mcserver.config import Config from mcserver.config import Config
from mcserver.models_auto import Exercise, TExercise, UpdateInfo
class ExerciseAPI(Resource):
"""The exercise API resource. It creates exercises for a given text."""
def __init__(self):
"""Initialize possible arguments for calls to the exercise REST API."""
# TODO: switch to other request parser, e.g. Marshmallow, because the one used by Flask-RESTful does not allow parsing arguments from different locations, e.g. one argument from 'location=args' and another argument from 'location=form'
self.reqparse: RequestParser = NetworkService.base_request_parser.copy()
self.reqparse.add_argument("urn", type=str, required=False, location="form", help="No URN provided")
self.reqparse.add_argument("type", type=str, required=False, location="form", help="No exercise type provided")
self.reqparse.add_argument("search_values", type=str, required=False, location="form",
help="No search value provided")
self.reqparse.add_argument("type_translation", type=str, location="form", required=False,
help="No exercise type translation provided")
self.reqparse.add_argument("work_author", type=str, location="form", required=False,
help="No work_author provided", default="")
self.reqparse.add_argument("work_title", type=str, required=False, location="form",
help="No work title provided", default="")
self.reqparse.add_argument("instructions", type=str, required=False, location="form", default="")
self.reqparse.add_argument("general_feedback", type=str, required=False, location="form", default=" ")
self.reqparse.add_argument("correct_feedback", type=str, required=False, location="form", default=" ")
self.reqparse.add_argument("partially_correct_feedback", type=str, required=False, location="form", default=" ")
self.reqparse.add_argument("incorrect_feedback", type=str, required=False, location="form", default=" ")
self.reqparse.add_argument("eid", type=str, required=False, location="args", help="No exercise ID provided")
super(ExerciseAPI, self).__init__()
def get(self):
args: dict = self.reqparse.parse_args()
eid: str = args["eid"]
exercise: Exercise = Exercise.query.filter_by(eid=eid).first()
if exercise is None:
abort(404)
ar: AnnisResponse = CorpusService.get_corpus(cts_urn=exercise.urn, is_csm=False)
if not ar.nodes:
abort(404)
exercise.last_access_time = datetime.utcnow()
db.session.commit()
exercise_type: ExerciseType = ExerciseType(exercise.exercise_type)
ar.solutions = json.loads(exercise.solutions)
ar.uri = exercise.uri
ar.exercise_id = exercise.eid
ar.exercise_type = exercise_type.value
return NetworkService.make_json_response(ar.__dict__)
def post(self):
# get request arguments
args: dict = self.reqparse.parse_args()
urn: str = args["urn"]
exercise_type: ExerciseType = ExerciseType(args["type"])
search_values_json: str = args["search_values"]
search_values_list: List[str] = json.loads(search_values_json)
aqls: List[str] = AnnotationService.map_search_values_to_aql(search_values_list=search_values_list,
exercise_type=exercise_type)
search_phenomena: List[Phenomenon] = [Phenomenon[x.split("=")[0]] for x in search_values_list]
# if there is custom text instead of a URN, immediately annotate it
conll_string_or_urn: str = urn if CorpusService.is_urn(urn) else AnnotationService.get_udpipe(
CorpusService.get_raw_text(urn, False))
# construct graph from CONLL data
response: dict = get_graph_data(title=urn, conll_string_or_urn=conll_string_or_urn, aqls=aqls,
exercise_type=exercise_type, search_phenomena=search_phenomena)
solutions_dict_list: List[Dict] = response["solutions"]
solutions: List[Solution] = [Solution(json_dict=x) for x in solutions_dict_list]
ar: AnnisResponse = make_new_exercise(graph_data_raw=response["graph_data_raw"], solutions=solutions, args=args,
conll=response["conll"], search_values=args["search_values"], urn=urn)
return NetworkService.make_json_response(ar.__dict__)
def adjust_solutions(exercise_data: ExerciseData, exercise_type: str, solutions: List[Solution]) -> List[Solution]: def adjust_solutions(exercise_data: ExerciseData, exercise_type: str, solutions: List[Solution]) -> List[Solution]:
...@@ -93,6 +25,23 @@ def adjust_solutions(exercise_data: ExerciseData, exercise_type: str, solutions: ...@@ -93,6 +25,23 @@ def adjust_solutions(exercise_data: ExerciseData, exercise_type: str, solutions:
return solutions return solutions
def get(eid: str) -> Union[Response, ConnexionResponse]:
exercise: TExercise = db.session.query(Exercise).filter_by(eid=eid).first()
if exercise is None:
return connexion.problem(404, Config.ERROR_TITLE_NOT_FOUND, Config.ERROR_MESSAGE_EXERCISE_NOT_FOUND)
ar: AnnisResponse = CorpusService.get_corpus(cts_urn=exercise.urn, is_csm=False)
if not ar.nodes:
return connexion.problem(404, Config.ERROR_TITLE_NOT_FOUND, Config.ERROR_MESSAGE_CORPUS_NOT_FOUND)
exercise.last_access_time = datetime.utcnow().timestamp()
db.session.commit()
exercise_type: ExerciseType = ExerciseType(exercise.exercise_type)
ar.solutions = json.loads(exercise.solutions)
ar.uri = NetworkService.get_exercise_uri(exercise)
ar.exercise_id = exercise.eid
ar.exercise_type = exercise_type.value
return NetworkService.make_json_response(ar.__dict__)
def get_graph_data(title: str, conll_string_or_urn: str, aqls: List[str], exercise_type: ExerciseType, def get_graph_data(title: str, conll_string_or_urn: str, aqls: List[str], exercise_type: ExerciseType,
search_phenomena: List[Phenomenon]): search_phenomena: List[Phenomenon]):
"""Sends annotated text data or a URN to the Corpus Storage Manager in order to get a graph.""" """Sends annotated text data or a URN to the Corpus Storage Manager in order to get a graph."""
...@@ -104,38 +53,35 @@ def get_graph_data(title: str, conll_string_or_urn: str, aqls: List[str], exerci ...@@ -104,38 +53,35 @@ def get_graph_data(title: str, conll_string_or_urn: str, aqls: List[str], exerci
try: try:
return json.loads(response.text) return json.loads(response.text)