Commit 0863b577 authored by Konstantin Schulz's avatar Konstantin Schulz

frontend now uses typescript interfaces that are generated automatically from...

frontend now uses typescript interfaces that are generated automatically from the OpenAPI specification
parent fe4ca88a
Pipeline #11663 passed with stages
in 2 minutes and 33 seconds
......@@ -27,7 +27,14 @@ Alternatively, you can use `ssh root@localhost -p 8022 -o "UserKnownHostsFile /d
To snapshot a running container, use `docker commit CONTAINER_ID`. It returns a snapshot ID, which you can access via `docker run -it SNAPSHOT_ID`.
## Models
To generate class structures for this project automatically:
1. Install OpenAPI Generator (using, e.g., `brew install openapi-generator`).
2. Run: `openapi-generator generate -i mc_backend/mcserver/mcserver_api.yaml -g typescript-angular -o mc_frontend/openapi/ && openapi-generator generate -i mc_backend/mcserver/mcserver_api.yaml -g python-flask -o mc_backend/openapi/ && python mc_backend/openapi_generator.py`.
## Documentation
### API
To view the API documentation, visit https://korpling.org/mc-service/mc/api/v1.0/ui/ .
### Changelog
To update the changelog, use: `git log --oneline --decorate > CHANGELOG`
......
......@@ -110,6 +110,9 @@ def init_logging(app: Flask, log_file_path: str):
app.logger.addHandler(file_handler)
app.logger.setLevel(log_level)
got_request_exception.connect(log_exception, app)
database_uri: str = app.config.get("SQLALCHEMY_DATABASE_URI", "")
database_uri = database_uri.split('@')[1] if '@' in database_uri else database_uri
app.logger.warning(f"Accessing database at: {database_uri}")
def log_exception(sender_app: Flask, exception, **extra):
......@@ -120,7 +123,7 @@ def log_exception(sender_app: Flask, exception, **extra):
exception -- the exception to be logged
**extra -- any additional arguments
"""
sender_app.logger.exception(f"ERROR for {flask.request.url}")
sender_app.logger.info(f"ERROR for {flask.request.url}")
def start_updater(app: Flask) -> Thread:
......
......@@ -6,14 +6,13 @@ from mcserver import Config
bp = Blueprint("api", __name__)
api = Api(bp)
from . import corpusAPI, corpusListAPI, exerciseAPI
from . import corpusAPI, corpusListAPI, exerciseAPI, staticExercisesAPI
from mcserver.app.api.exerciseListAPI import ExerciseListAPI
from mcserver.app.api.fileAPI import FileAPI
from mcserver.app.api.frequencyAPI import FrequencyAPI
from mcserver.app.api.h5pAPI import H5pAPI
from mcserver.app.api.kwicAPI import KwicAPI
from mcserver.app.api.rawTextAPI import RawTextAPI
from mcserver.app.api.staticExercisesAPI import StaticExercisesAPI
from mcserver.app.api.textcomplexityAPI import TextComplexityAPI
from mcserver.app.api.validReffAPI import ValidReffAPI
from mcserver.app.api.vectorNetworkAPI import VectorNetworkAPI
......@@ -25,7 +24,6 @@ api.add_resource(FrequencyAPI, Config.SERVER_URI_FREQUENCY, endpoint="frequency"
api.add_resource(H5pAPI, Config.SERVER_URI_H5P, endpoint="h5p")
api.add_resource(KwicAPI, Config.SERVER_URI_KWIC, endpoint="kwic")
api.add_resource(RawTextAPI, Config.SERVER_URI_RAW_TEXT, endpoint="rawtext")
api.add_resource(StaticExercisesAPI, Config.SERVER_URI_STATIC_EXERCISES, endpoint="exercises")
api.add_resource(TextComplexityAPI, Config.SERVER_URI_TEXT_COMPLEXITY, endpoint='textcomplexity')
api.add_resource(ValidReffAPI, Config.SERVER_URI_VALID_REFF, endpoint="validReff")
api.add_resource(VectorNetworkAPI, Config.SERVER_URI_VECTOR_NETWORK, endpoint="vectorNetwork")
......
......@@ -7,12 +7,12 @@ from decimal import Decimal, ROUND_HALF_UP
from io import BytesIO
from tempfile import mkstemp
from time import time
from typing import Dict, List, Set, Match, Tuple
from typing import Dict, List, Set, Match, Tuple, Union
from zipfile import ZipFile
import connexion
import requests
from flask_restful import Resource, abort
from flask_restful.reqparse import RequestParser
from connexion.lifecycle import ConnexionResponse
from requests import Response
from mcserver.app.models import StaticExercise
......@@ -20,22 +20,14 @@ from mcserver.app.services import NetworkService, AnnotationService
from mcserver.config import Config
class StaticExercisesAPI(Resource):
"""The StaticExercises API resource. It guides users to static language exercises in the frontend."""
def __init__(self):
"""Initialize possible arguments for calls to the StaticExercises REST API."""
self.reqparse: RequestParser = NetworkService.base_request_parser.copy()
super(StaticExercisesAPI, self).__init__()
def get(self):
""" The GET method for the StaticExercises REST API. It provides a list of static exercises
and their respective URLs in the frontend. """
# TODO: WRITE AND READ LAST UPDATE TIME FROM THE DATABASE
if datetime.fromtimestamp(time() - Config.INTERVAL_STATIC_EXERCISES) > NetworkService.exercises_last_update or \
len(NetworkService.exercises) == 0:
update_exercises()
return NetworkService.make_json_response({k: v.__dict__ for (k, v) in NetworkService.exercises.items()})
def get() -> Union[Response, ConnexionResponse]:
""" The GET method for the StaticExercises REST API. It provides a list of static exercises
and their respective URLs in the frontend. """
# TODO: WRITE AND READ LAST UPDATE TIME FROM THE DATABASE
if datetime.fromtimestamp(time() - Config.INTERVAL_STATIC_EXERCISES) > NetworkService.exercises_last_update \
or len(NetworkService.exercises) == 0:
return update_exercises()
return NetworkService.make_json_response({k: v.__dict__ for (k, v) in NetworkService.exercises.items()})
def get_relevant_strings(response: Response):
......@@ -121,12 +113,13 @@ def handle_voc_list(content: dict, url: str, relevant_strings_dict: Dict[str, Se
relevant_strings_dict[url].add(match_parts[0])
def update_exercises():
def update_exercises() -> Union[Response, ConnexionResponse]:
""" Gets all static exercises from the frontend code repository and looks for the lemmata in them."""
# TODO: check last update of the directory before pulling the whole zip archive
response: Response = requests.get(Config.STATIC_EXERCISES_REPOSITORY_URL, stream=True)
if not response.ok:
abort(503)
return connexion.problem(
503, Config.ERROR_TITLE_SERVICE_UNAVAILABLE, Config.ERROR_MESSAGE_SERVICE_UNAVAILABLE)
relevant_strings_dict: Dict[str, Set[str]] = get_relevant_strings(response)
file_dict: Dict = {}
lemma_set: Set[str] = set()
......@@ -150,3 +143,4 @@ def update_exercises():
word = word[:-1]
NetworkService.exercises[url].solutions.append(list(search_results[search_results_dict[word]]))
NetworkService.exercises_last_update = datetime.fromtimestamp(time())
return NetworkService.make_json_response({k: v.__dict__ for (k, v) in NetworkService.exercises.items()})
......@@ -4,13 +4,13 @@ from enum import Enum
import typing
from mcserver.config import Config
from mcserver.models_auto import TExercise, Corpus, TCorpus, Exercise, TLearningResult, LearningResult
from openapi.openapi_server.models import SolutionElement, Solution, Link, Node, TextComplexity, AnnisResponse, \
from openapi.openapi_server.models import SolutionElement, Solution, Link, NodeMC, TextComplexity, AnnisResponse, \
GraphData
AnnisResponse = AnnisResponse
GraphData = GraphData
LinkMC = Link
NodeMC = Node
NodeMC = NodeMC
SolutionElement = SolutionElement
TextComplexity = TextComplexity
......
......@@ -104,7 +104,7 @@ class CorpusService:
# there is actually no text, only a URN, so we need to get it ourselves
url: str = f"{Config.INTERNET_PROTOCOL}{Config.HOST_IP_CSM}:{Config.CORPUS_STORAGE_MANAGER_PORT}/"
response: requests.Response = requests.get(url, params=dict(urn=cts_urn))
return AnnisResponse(graph_data=GraphData.from_dict(json.loads(response.text)))
return AnnisResponse.from_dict(json.loads(response.text))
@staticmethod
def get_frequency_analysis(urn: str, is_csm: bool) -> FrequencyAnalysis:
......
......@@ -74,10 +74,14 @@ class Config(object):
DOCKER_SERVICE_NAME_MCSERVER = "mcserver"
ERROR_MESSAGE_CORPUS_NOT_FOUND = "A corpus with the specified ID was not found!"
ERROR_MESSAGE_EXERCISE_NOT_FOUND = "An exercise with the specified ID was not found!"
ERROR_MESSAGE_INTERNAL_SERVER_ERROR = "The server encountered an unexpected condition that prevented it from " \
"fulfilling the request."
ERROR_MESSAGE_INTERNAL_SERVER_ERROR = \
"The server encountered an unexpected condition that prevented it from fulfilling the request."
ERROR_MESSAGE_SERVICE_UNAVAILABLE = \
"The server is currently unable to handle the request due to a temporary overload or scheduled " \
"maintenance, which will likely be alleviated after some delay."
ERROR_TITLE_INTERNAL_SERVER_ERROR = "Internal Server Error"
ERROR_TITLE_NOT_FOUND = "Not found"
ERROR_TITLE_SERVICE_UNAVAILABLE = "Service Unavailable"
FAVICON_FILE_NAME = "favicon.ico"
FLASK_MIGRATE = "migrate"
GRAPHANNIS_DEPENDENCY_LINK = "dep"
......
......@@ -118,38 +118,26 @@ paths:
schema:
$ref: '#/components/schemas/AnnisResponse'
requestBody:
$ref: '#/components/requestBodies/ExerciseForm'
components:
requestBodies:
ExerciseForm:
required: true
content:
application/x-www-form-urlencoded:
schema:
x-body-name: exercise_data
type: object
allOf:
- $ref: '#/components/schemas/ExerciseBase'
required:
- instructions
- search_values
- description: Additional exercise data.
required: true
content:
application/x-www-form-urlencoded:
schema:
x-body-name: exercise_data
type: object
$ref: '#/components/schemas/ExerciseForm'
/exercises:
get:
summary: Returns metadata for static exercises.
operationId: mcserver.app.api.staticExercisesAPI.get
responses:
200:
description: Metadata for static exercises, including their respective URIs in the frontend.
content:
application/json:
schema:
# TODO: specify object properties
type: object
properties:
type:
type: string
description: Type of exercise, concerning interaction and layout.
example: markWords
type_translation:
type: string
description: Localized expression of the exercise type.
example: Cloze
urn:
type: string
description: CTS URN for the text passage from which the exercise was created.
example: urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1
required:
- type
components:
schemas:
AnnisResponse:
description: A response with graph data from ANNIS, possibly with additional data for exercises.
......@@ -167,24 +155,7 @@ components:
type: array
description: List of items with frequency data for linguistic phenomena.
items:
type: object
properties:
count:
type: integer
description: How often the given combination of values occurred.
example: 1
phenomena:
type: array
description: Labels for the phenomena described in this frequency entry.
example: []
items:
type: string
values:
type: array
description: Values for the phenomena described in this frequency entry.
example: []
items:
type: string
$ref: "#/components/schemas/FrequencyItem"
graph_data:
$ref: "#/components/schemas/GraphData"
solutions:
......@@ -348,6 +319,48 @@ components:
description: Title of the base text for the exercise.
example: Noctes Atticae
default: ""
required:
- instructions
- search_values
ExerciseForm:
allOf:
- $ref: '#/components/schemas/ExerciseBase'
- description: Additional exercise data.
type: object
properties:
type:
type: string
description: Type of exercise, concerning interaction and layout.
example: markWords
type_translation:
type: string
description: Localized expression of the exercise type.
example: Cloze
urn:
type: string
description: CTS URN for the text passage from which the exercise was created.
example: urn:cts:latinLit:phi0448.phi001.perseus-lat2:1.1.1
required:
- type
FrequencyItem:
type: object
properties:
count:
type: integer
description: How often the given combination of values occurred.
example: 1
phenomena:
type: array
description: Labels for the phenomena described in this frequency entry.
example: []
items:
type: string
values:
type: array
description: Values for the phenomena described in this frequency entry.
example: []
items:
type: string
GraphData:
type: object
description: Nodes, edges and metadata for a graph.
......@@ -373,7 +386,7 @@ components:
type: array
description: List of nodes for the graph.
items:
$ref: '#/components/schemas/Node'
$ref: '#/components/schemas/NodeMC'
required:
- links
- nodes
......@@ -520,12 +533,7 @@ components:
type: string
description: Dependency relation described by the edge.
example: "det"
required:
- annis_component_name
- annis_component_type
- source
- target
Node:
NodeMC:
type: object
properties:
annis_node_name:
......@@ -572,14 +580,6 @@ components:
type: string
description: Solution value for this node in an exercise.
example: ""
required:
- annis_node_name
- annis_node_type
- annis_tok
- annis_type
- id
- udep_lemma
- udep_upostag
Solution:
type: object
description: Correct solution for an exercise.
......@@ -609,7 +609,6 @@ components:
description: Unique identifier for the token in a sentence.
example: 9
required:
- content
- sentence_id
- token_id
TextComplexity:
......
......@@ -149,6 +149,8 @@ Corpus: TCorpus = models.Corpus # type: ignore
class _ExerciseDictBase(typing.TypedDict, total=True):
"""TypedDict for properties that are required."""
instructions: str
search_values: str
eid: str
last_access_time: float
......@@ -159,9 +161,7 @@ class ExerciseDict(_ExerciseDictBase, total=False):
correct_feedback: str
general_feedback: str
incorrect_feedback: str
instructions: str
partially_correct_feedback: str
search_values: str
work_author: str
work_title: str
conll: str
......@@ -233,14 +233,14 @@ class TExercise(typing.Protocol):
def __init__(
self,
instructions: str,
search_values: str,
eid: str,
last_access_time: float,
correct_feedback: str = "",
general_feedback: str = "",
incorrect_feedback: str = "",
instructions: str = "",
partially_correct_feedback: str = "",
search_values: str = "[]",
work_author: str = "",
work_title: str = "",
conll: str = "",
......@@ -289,14 +289,14 @@ class TExercise(typing.Protocol):
@classmethod
def from_dict(
cls,
instructions: str,
search_values: str,
eid: str,
last_access_time: float,
correct_feedback: str = "",
general_feedback: str = "",
incorrect_feedback: str = "",
instructions: str = "",
partially_correct_feedback: str = "",
search_values: str = "[]",
work_author: str = "",
work_title: str = "",
conll: str = "",
......
......@@ -98,7 +98,7 @@ class TestHelper:
if not len(Mocks.app_dict):
with patch.object(TextService, "init_stop_words_latin"):
Mocks.app_dict[class_name] = TestHelper(app_factory(TestingConfig))
Mocks.app_dict[class_name].app.logger.setLevel(logging.CRITICAL)
Mocks.app_dict[class_name].app.logger.setLevel(logging.WARNING)
Mocks.app_dict[class_name].app.testing = True
db.session.commit()
......
......@@ -3,8 +3,6 @@ import six
from openapi.openapi_server.models.annis_response import AnnisResponse # noqa: E501
from openapi.openapi_server.models.corpus import Corpus # noqa: E501
from openapi.openapi_server.models.exercise_base import ExerciseBase # noqa: E501
from openapi.openapi_server.models.unknownbasetype import UNKNOWN_BASE_TYPE # noqa: E501
from openapi.openapi_server import util
......@@ -79,16 +77,23 @@ def mcserver_app_api_exercise_api_get(eid): # noqa: E501
return 'do some magic!'
def mcserver_app_api_exercise_api_post(unknown_base_type): # noqa: E501
def mcserver_app_api_exercise_api_post(): # noqa: E501
"""Creates a new exercise.
# noqa: E501
:param unknown_base_type:
:type unknown_base_type: dict | bytes
:rtype: AnnisResponse
"""
if connexion.request.is_json:
unknown_base_type = UNKNOWN_BASE_TYPE.from_dict(connexion.request.get_json()) # noqa: E501
return 'do some magic!'
def mcserver_app_api_static_exercises_api_get(): # noqa: E501
"""Returns metadata for static exercises.
# noqa: E501
:rtype: object
"""
return 'do some magic!'
......@@ -4,15 +4,17 @@
from __future__ import absolute_import
# import models into model package
from openapi.openapi_server.models.annis_response import AnnisResponse
from openapi.openapi_server.models.annis_response_frequency_analysis import AnnisResponseFrequencyAnalysis
from openapi.openapi_server.models.corpus import Corpus
from openapi.openapi_server.models.exercise import Exercise
from openapi.openapi_server.models.exercise_all_of import ExerciseAllOf
from openapi.openapi_server.models.exercise_base import ExerciseBase
from openapi.openapi_server.models.exercise_form import ExerciseForm
from openapi.openapi_server.models.exercise_form_all_of import ExerciseFormAllOf
from openapi.openapi_server.models.frequency_item import FrequencyItem
from openapi.openapi_server.models.graph_data import GraphData
from openapi.openapi_server.models.learning_result import LearningResult
from openapi.openapi_server.models.link import Link
from openapi.openapi_server.models.node import Node
from openapi.openapi_server.models.node_mc import NodeMC
from openapi.openapi_server.models.solution import Solution
from openapi.openapi_server.models.solution_element import SolutionElement
from openapi.openapi_server.models.text_complexity import TextComplexity
......
......@@ -6,13 +6,13 @@ from datetime import date, datetime # noqa: F401
from typing import List, Dict # noqa: F401
from openapi.openapi_server.models.base_model_ import Model
from openapi.openapi_server.models.annis_response_frequency_analysis import AnnisResponseFrequencyAnalysis
from openapi.openapi_server.models.frequency_item import FrequencyItem
from openapi.openapi_server.models.graph_data import GraphData
from openapi.openapi_server.models.solution import Solution
from openapi.openapi_server.models.text_complexity import TextComplexity
from openapi.openapi_server import util
from openapi.openapi_server.models.annis_response_frequency_analysis import AnnisResponseFrequencyAnalysis # noqa: E501
from openapi.openapi_server.models.frequency_item import FrequencyItem # noqa: E501
from openapi.openapi_server.models.graph_data import GraphData # noqa: E501
from openapi.openapi_server.models.solution import Solution # noqa: E501
from openapi.openapi_server.models.text_complexity import TextComplexity # noqa: E501
......@@ -31,7 +31,7 @@ class AnnisResponse(Model):
:param exercise_type: The exercise_type of this AnnisResponse. # noqa: E501
:type exercise_type: str
:param frequency_analysis: The frequency_analysis of this AnnisResponse. # noqa: E501
:type frequency_analysis: List[AnnisResponseFrequencyAnalysis]
:type frequency_analysis: List[FrequencyItem]
:param graph_data: The graph_data of this AnnisResponse. # noqa: E501
:type graph_data: GraphData
:param solutions: The solutions of this AnnisResponse. # noqa: E501
......@@ -44,7 +44,7 @@ class AnnisResponse(Model):
self.openapi_types = {
'exercise_id': str,
'exercise_type': str,
'frequency_analysis': List[AnnisResponseFrequencyAnalysis],
'frequency_analysis': List[FrequencyItem],
'graph_data': GraphData,
'solutions': List[Solution],
'text_complexity': TextComplexity,
......@@ -133,7 +133,7 @@ class AnnisResponse(Model):
List of items with frequency data for linguistic phenomena. # noqa: E501
:return: The frequency_analysis of this AnnisResponse.
:rtype: List[AnnisResponseFrequencyAnalysis]
:rtype: List[FrequencyItem]
"""
return self._frequency_analysis
......@@ -144,7 +144,7 @@ class AnnisResponse(Model):
List of items with frequency data for linguistic phenomena. # noqa: E501
:param frequency_analysis: The frequency_analysis of this AnnisResponse.
:type frequency_analysis: List[AnnisResponseFrequencyAnalysis]
:type frequency_analysis: List[FrequencyItem]
"""
self._frequency_analysis = frequency_analysis
......
......@@ -215,6 +215,8 @@ class Exercise(Model):
:param instructions: The instructions of this Exercise.
:type instructions: str
"""
if instructions is None:
raise ValueError("Invalid value for `instructions`, must not be `None`") # noqa: E501
self._instructions = instructions
......@@ -261,6 +263,8 @@ class Exercise(Model):
:param search_values: The search_values of this Exercise.
:type search_values: str
"""
if search_values is None:
raise ValueError("Invalid value for `search_values`, must not be `None`") # noqa: E501
self._search_values = search_values
......
......@@ -166,6 +166,8 @@ class ExerciseBase(Model):
:param instructions: The instructions of this ExerciseBase.
:type instructions: str
"""
if instructions is None:
raise ValueError("Invalid value for `instructions`, must not be `None`") # noqa: E501
self._instructions = instructions
......@@ -212,6 +214,8 @@ class ExerciseBase(Model):
:param search_values: The search_values of this ExerciseBase.
:type search_values: str
"""
if search_values is None:
raise ValueError("Invalid value for `search_values`, must not be `None`") # noqa: E501
self._search_values = search_values
......
# coding: utf-8
from __future__ import absolute_import
from datetime import date, datetime # noqa: F401
from typing import List, Dict # noqa: F401
from openapi.openapi_server.models.base_model_ import Model
from openapi.openapi_server.models.exercise_base import ExerciseBase
from openapi.openapi_server.models.exercise_form_all_of import ExerciseFormAllOf
from openapi.openapi_server import util
from openapi.openapi_server.models.exercise_base import ExerciseBase # noqa: E501
from openapi.openapi_server.models.exercise_form_all_of import ExerciseFormAllOf # noqa: E501
class ExerciseForm(Model):
"""NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
Do not edit the class manually.
"""
def __init__(self, correct_feedback='', general_feedback='', incorrect_feedback='', instructions='', partially_correct_feedback='', search_values='[]', work_author='', work_title='', type=None, type_translation=None, urn=None): # noqa: E501
"""ExerciseForm - a model defined in OpenAPI
:param correct_feedback: The correct_feedback of this ExerciseForm. # noqa: E501
:type correct_feedback: str
:param general_feedback: The general_feedback of this ExerciseForm. # noqa: E501
:type general_feedback: str
:param incorrect_feedback: The incorrect_feedback of this ExerciseForm. # noqa: E501
:type incorrect_feedback: str
:param instructions: The instructions of this ExerciseForm. # noqa: E501
:type instructions: str
:param pa