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 ...@@ -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`. 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 ## Documentation
### API
To view the API documentation, visit https://korpling.org/mc-service/mc/api/v1.0/ui/ .
### Changelog ### Changelog
To update the changelog, use: `git log --oneline --decorate > CHANGELOG` To update the changelog, use: `git log --oneline --decorate > CHANGELOG`
......
...@@ -110,6 +110,9 @@ def init_logging(app: Flask, log_file_path: str): ...@@ -110,6 +110,9 @@ def init_logging(app: Flask, log_file_path: str):
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
app.logger.setLevel(log_level) app.logger.setLevel(log_level)
got_request_exception.connect(log_exception, app) 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): def log_exception(sender_app: Flask, exception, **extra):
...@@ -120,7 +123,7 @@ 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 exception -- the exception to be logged
**extra -- any additional arguments **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: def start_updater(app: Flask) -> Thread:
......
...@@ -6,14 +6,13 @@ from mcserver import Config ...@@ -6,14 +6,13 @@ from mcserver import Config
bp = Blueprint("api", __name__) bp = Blueprint("api", __name__)
api = Api(bp) 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.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
from mcserver.app.api.h5pAPI import H5pAPI from mcserver.app.api.h5pAPI import H5pAPI
from mcserver.app.api.kwicAPI import KwicAPI from mcserver.app.api.kwicAPI import KwicAPI
from mcserver.app.api.rawTextAPI import RawTextAPI 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.textcomplexityAPI import TextComplexityAPI
from mcserver.app.api.validReffAPI import ValidReffAPI from mcserver.app.api.validReffAPI import ValidReffAPI
from mcserver.app.api.vectorNetworkAPI import VectorNetworkAPI from mcserver.app.api.vectorNetworkAPI import VectorNetworkAPI
...@@ -25,7 +24,6 @@ api.add_resource(FrequencyAPI, Config.SERVER_URI_FREQUENCY, endpoint="frequency" ...@@ -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(H5pAPI, Config.SERVER_URI_H5P, endpoint="h5p")
api.add_resource(KwicAPI, Config.SERVER_URI_KWIC, endpoint="kwic") api.add_resource(KwicAPI, Config.SERVER_URI_KWIC, endpoint="kwic")
api.add_resource(RawTextAPI, Config.SERVER_URI_RAW_TEXT, endpoint="rawtext") 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(TextComplexityAPI, Config.SERVER_URI_TEXT_COMPLEXITY, endpoint='textcomplexity')
api.add_resource(ValidReffAPI, Config.SERVER_URI_VALID_REFF, endpoint="validReff") api.add_resource(ValidReffAPI, Config.SERVER_URI_VALID_REFF, endpoint="validReff")
api.add_resource(VectorNetworkAPI, Config.SERVER_URI_VECTOR_NETWORK, endpoint="vectorNetwork") api.add_resource(VectorNetworkAPI, Config.SERVER_URI_VECTOR_NETWORK, endpoint="vectorNetwork")
......
...@@ -7,12 +7,12 @@ from decimal import Decimal, ROUND_HALF_UP ...@@ -7,12 +7,12 @@ from decimal import Decimal, ROUND_HALF_UP
from io import BytesIO from io import BytesIO
from tempfile import mkstemp from tempfile import mkstemp
from time import time 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 from zipfile import ZipFile
import connexion
import requests import requests
from flask_restful import Resource, abort from connexion.lifecycle import ConnexionResponse
from flask_restful.reqparse import RequestParser
from requests import Response from requests import Response
from mcserver.app.models import StaticExercise from mcserver.app.models import StaticExercise
...@@ -20,22 +20,14 @@ from mcserver.app.services import NetworkService, AnnotationService ...@@ -20,22 +20,14 @@ from mcserver.app.services import NetworkService, AnnotationService
from mcserver.config import Config from mcserver.config import Config
class StaticExercisesAPI(Resource): def get() -> Union[Response, ConnexionResponse]:
"""The StaticExercises API resource. It guides users to static language exercises in the frontend.""" """ The GET method for the StaticExercises REST API. It provides a list of static exercises
and their respective URLs in the frontend. """
def __init__(self): # TODO: WRITE AND READ LAST UPDATE TIME FROM THE DATABASE
"""Initialize possible arguments for calls to the StaticExercises REST API.""" if datetime.fromtimestamp(time() - Config.INTERVAL_STATIC_EXERCISES) > NetworkService.exercises_last_update \
self.reqparse: RequestParser = NetworkService.base_request_parser.copy() or len(NetworkService.exercises) == 0:
super(StaticExercisesAPI, self).__init__() return update_exercises()
return NetworkService.make_json_response({k: v.__dict__ for (k, v) in NetworkService.exercises.items()})
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_relevant_strings(response: Response): def get_relevant_strings(response: Response):
...@@ -121,12 +113,13 @@ def handle_voc_list(content: dict, url: str, relevant_strings_dict: Dict[str, Se ...@@ -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]) 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.""" """ 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 # TODO: check last update of the directory before pulling the whole zip archive
response: Response = requests.get(Config.STATIC_EXERCISES_REPOSITORY_URL, stream=True) response: Response = requests.get(Config.STATIC_EXERCISES_REPOSITORY_URL, stream=True)
if not response.ok: 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) relevant_strings_dict: Dict[str, Set[str]] = get_relevant_strings(response)
file_dict: Dict = {} file_dict: Dict = {}
lemma_set: Set[str] = set() lemma_set: Set[str] = set()
...@@ -150,3 +143,4 @@ def update_exercises(): ...@@ -150,3 +143,4 @@ def update_exercises():
word = word[:-1] word = word[:-1]
NetworkService.exercises[url].solutions.append(list(search_results[search_results_dict[word]])) NetworkService.exercises[url].solutions.append(list(search_results[search_results_dict[word]]))
NetworkService.exercises_last_update = datetime.fromtimestamp(time()) 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 ...@@ -4,13 +4,13 @@ from enum import Enum
import typing import typing
from mcserver.config import Config from mcserver.config import Config
from mcserver.models_auto import TExercise, Corpus, TCorpus, Exercise, TLearningResult, LearningResult 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 GraphData
AnnisResponse = AnnisResponse AnnisResponse = AnnisResponse
GraphData = GraphData GraphData = GraphData
LinkMC = Link LinkMC = Link
NodeMC = Node NodeMC = NodeMC
SolutionElement = SolutionElement SolutionElement = SolutionElement
TextComplexity = TextComplexity TextComplexity = TextComplexity
......
...@@ -104,7 +104,7 @@ class CorpusService: ...@@ -104,7 +104,7 @@ class CorpusService:
# there is actually no text, only a URN, so we need to get it ourselves # 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}/" 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)) 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 @staticmethod
def get_frequency_analysis(urn: str, is_csm: bool) -> FrequencyAnalysis: def get_frequency_analysis(urn: str, is_csm: bool) -> FrequencyAnalysis:
......
...@@ -74,10 +74,14 @@ class Config(object): ...@@ -74,10 +74,14 @@ class Config(object):
DOCKER_SERVICE_NAME_MCSERVER = "mcserver" DOCKER_SERVICE_NAME_MCSERVER = "mcserver"
ERROR_MESSAGE_CORPUS_NOT_FOUND = "A corpus with the specified ID was not found!" 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_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 " \ ERROR_MESSAGE_INTERNAL_SERVER_ERROR = \
"fulfilling the request." "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_INTERNAL_SERVER_ERROR = "Internal Server Error"
ERROR_TITLE_NOT_FOUND = "Not found" ERROR_TITLE_NOT_FOUND = "Not found"
ERROR_TITLE_SERVICE_UNAVAILABLE = "Service Unavailable"
FAVICON_FILE_NAME = "favicon.ico" FAVICON_FILE_NAME = "favicon.ico"
FLASK_MIGRATE = "migrate" FLASK_MIGRATE = "migrate"
GRAPHANNIS_DEPENDENCY_LINK = "dep" GRAPHANNIS_DEPENDENCY_LINK = "dep"
......
...@@ -118,38 +118,26 @@ paths: ...@@ -118,38 +118,26 @@ paths:
schema: schema:
$ref: '#/components/schemas/AnnisResponse' $ref: '#/components/schemas/AnnisResponse'
requestBody: requestBody:
$ref: '#/components/requestBodies/ExerciseForm' required: true
components: content:
requestBodies: application/x-www-form-urlencoded:
ExerciseForm: schema:
required: true x-body-name: exercise_data
content: type: object
application/x-www-form-urlencoded: $ref: '#/components/schemas/ExerciseForm'
schema: /exercises:
x-body-name: exercise_data get:
type: object summary: Returns metadata for static exercises.
allOf: operationId: mcserver.app.api.staticExercisesAPI.get
- $ref: '#/components/schemas/ExerciseBase' responses:
required: 200:
- instructions description: Metadata for static exercises, including their respective URIs in the frontend.
- search_values content:
- description: Additional exercise data. application/json:
schema:
# TODO: specify object properties
type: object type: object
properties: components:
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
schemas: schemas:
AnnisResponse: AnnisResponse:
description: A response with graph data from ANNIS, possibly with additional data for exercises. description: A response with graph data from ANNIS, possibly with additional data for exercises.
...@@ -167,24 +155,7 @@ components: ...@@ -167,24 +155,7 @@ components:
type: array type: array
description: List of items with frequency data for linguistic phenomena. description: List of items with frequency data for linguistic phenomena.
items: items:
type: object $ref: "#/components/schemas/FrequencyItem"
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
graph_data: graph_data:
$ref: "#/components/schemas/GraphData" $ref: "#/components/schemas/GraphData"
solutions: solutions:
...@@ -348,6 +319,48 @@ components: ...@@ -348,6 +319,48 @@ components:
description: Title of the base text for the exercise. description: Title of the base text for the exercise.
example: Noctes Atticae example: Noctes Atticae
default: "" 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: GraphData:
type: object type: object
description: Nodes, edges and metadata for a graph. description: Nodes, edges and metadata for a graph.
...@@ -373,7 +386,7 @@ components: ...@@ -373,7 +386,7 @@ components:
type: array type: array
description: List of nodes for the graph. description: List of nodes for the graph.
items: items:
$ref: '#/components/schemas/Node' $ref: '#/components/schemas/NodeMC'
required: required:
- links - links
- nodes - nodes
...@@ -520,12 +533,7 @@ components: ...@@ -520,12 +533,7 @@ components:
type: string type: string
description: Dependency relation described by the edge. description: Dependency relation described by the edge.
example: "det" example: "det"
required: NodeMC:
- annis_component_name
- annis_component_type
- source
- target
Node:
type: object type: object
properties: properties:
annis_node_name: annis_node_name:
...@@ -572,14 +580,6 @@ components: ...@@ -572,14 +580,6 @@ components:
type: string type: string
description: Solution value for this node in an exercise. description: Solution value for this node in an exercise.
example: "" example: ""
required:
- annis_node_name
- annis_node_type
- annis_tok
- annis_type
- id
- udep_lemma
- udep_upostag
Solution: Solution:
type: object type: object
description: Correct solution for an exercise. description: Correct solution for an exercise.
...@@ -609,7 +609,6 @@ components: ...@@ -609,7 +609,6 @@ components:
description: Unique identifier for the token in a sentence. description: Unique identifier for the token in a sentence.
example: 9 example: 9
required: required:
- content
- sentence_id - sentence_id
- token_id - token_id
TextComplexity: TextComplexity:
......
...@@ -149,6 +149,8 @@ Corpus: TCorpus = models.Corpus # type: ignore ...@@ -149,6 +149,8 @@ Corpus: TCorpus = models.Corpus # type: ignore
class _ExerciseDictBase(typing.TypedDict, total=True): class _ExerciseDictBase(typing.TypedDict, total=True):
"""TypedDict for properties that are required.""" """TypedDict for properties that are required."""
instructions: str
search_values: str
eid: str eid: str
last_access_time: float last_access_time: float
...@@ -159,9 +161,7 @@ class ExerciseDict(_ExerciseDictBase, total=False): ...@@ -159,9 +161,7 @@ class ExerciseDict(_ExerciseDictBase, total=False):
correct_feedback: str correct_feedback: str
general_feedback: str general_feedback: str
incorrect_feedback: str incorrect_feedback: str
instructions: str
partially_correct_feedback: str partially_correct_feedback: str
search_values: str
work_author: str work_author: str
work_title: str work_title: str
conll: str conll: str
...@@ -233,14 +233,14 @@ class TExercise(typing.Protocol): ...@@ -233,14 +233,14 @@ class TExercise(typing.Protocol):
def __init__( def __init__(
self, self,
instructions: str,
search_values: str,
eid: str, eid: str,
last_access_time: float, last_access_time: float,
correct_feedback: str = "", correct_feedback: str = "",
general_feedback: str = "", general_feedback: str = "",
incorrect_feedback: str = "", incorrect_feedback: str = "",
instructions: str = "",
partially_correct_feedback: str = "", partially_correct_feedback: str = "",
search_values: str = "[]",
work_author: str = "", work_author: str = "",
work_title: str = "", work_title: str = "",
conll: str = "", conll: str = "",
...@@ -289,14 +289,14 @@ class TExercise(typing.Protocol): ...@@ -289,14 +289,14 @@ class TExercise(typing.Protocol):
@classmethod @classmethod
def from_dict( def from_dict(
cls, cls,
instructions: str,
search_values: str,
eid: str, eid: str,
last_access_time: float, last_access_time: float,
correct_feedback: str = "", correct_feedback: str = "",
general_feedback: str = "", general_feedback: str = "",
incorrect_feedback: str = "", incorrect_feedback: str = "",
instructions: str = "",
partially_correct_feedback: str = "", partially_correct_feedback: str = "",
search_values: str = "[]",
work_author: str = "", work_author: str = "",
work_title: str = "", work_title: str = "",
conll: str = "", conll: str = "",
......
...@@ -98,7 +98,7 @@ class TestHelper: ...@@ -98,7 +98,7 @@ class TestHelper:
if not len(Mocks.app_dict): if not len(Mocks.app_dict):
with patch.object(TextService, "init_stop_words_latin"): with patch.object(TextService, "init_stop_words_latin"):
Mocks.app_dict[class_name] = TestHelper(app_factory(TestingConfig)) 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 Mocks.app_dict[class_name].app.testing = True
db.session.commit() db.session.commit()
......
...@@ -3,8 +3,6 @@ import six ...@@ -3,8 +3,6 @@ import six
from openapi.openapi_server.models.annis_response import AnnisResponse # noqa: E501 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.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 from openapi.openapi_server import util
...@@ -79,16 +77,23 @@ def mcserver_app_api_exercise_api_get(eid): # noqa: E501 ...@@ -79,16 +77,23 @@ def mcserver_app_api_exercise_api_get(eid): # noqa: E501
return 'do some magic!' return 'do some magic!'