diff --git a/backend/aethel_db/apps.py b/backend/aethel_db/apps.py index f98b654..6c6fe6d 100644 --- a/backend/aethel_db/apps.py +++ b/backend/aethel_db/apps.py @@ -1,6 +1,18 @@ +import logging +import os +from django.conf import settings from django.apps import AppConfig +from .models import load_dataset +from parseport.logger import logger + class AethelDbConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "aethel_db" + + def ready(self): + if os.path.exists(settings.DATASET_PATH): + load_dataset() + else: + logger.critical("Æthel dataset not found.") diff --git a/backend/aethel_db/management/commands/create_aethel_subset.py b/backend/aethel_db/management/commands/create_aethel_subset.py index 247889a..4fe3216 100644 --- a/backend/aethel_db/management/commands/create_aethel_subset.py +++ b/backend/aethel_db/management/commands/create_aethel_subset.py @@ -1,16 +1,17 @@ import pickle -from django.conf import settings from django.core.management.base import BaseCommand, CommandParser from parseport.logger import logger -FULL_DATASET_PATH = getattr(settings, "FULL_DATASET_PATH") -DATA_SUBSET_PATH = getattr(settings, "DATA_SUBSET_PATH") class Command(BaseCommand): + requires_system_checks = [] + help = "Creates a subset of the Aethel dataset and outputs it to a new pickle file. Retrieves the passed number of records (default = 50) from each of the three subsets included: 'train', 'dev', and 'test'." def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument('src', help="Path to dataset (pickle format)") + parser.add_argument('dst', help="Path to subset output") parser.add_argument( "--number-of-records", "-n", @@ -32,7 +33,7 @@ def handle(self, *args, **options): subset_size = options["number-of-records"] logger.info(f"Creating a subset of the Aethel dataset with size {subset_size}...") - with open(FULL_DATASET_PATH, "rb") as f: + with open(options['src'], "rb") as f: version, (train, dev, test) = pickle.load(f) logger.info("Full pickle loaded!") @@ -52,7 +53,7 @@ def handle(self, *args, **options): new_dev = dev[:clamped] logger.info("Writing smaller dataset to new pickle file...") - with open(DATA_SUBSET_PATH, "wb") as f: + with open(options['dst'], "wb") as f: pickle_contents = version, (new_train, new_dev, new_test) pickle.dump(pickle_contents, f) diff --git a/backend/aethel_db/models.py b/backend/aethel_db/models.py index 71a8362..54bf50d 100644 --- a/backend/aethel_db/models.py +++ b/backend/aethel_db/models.py @@ -1,3 +1,12 @@ -from django.db import models +from typing import Optional -# Create your models here. +from django.conf import settings + +from aethel import ProofBank + +dataset: Optional[ProofBank] = None + + +def load_dataset(): + global dataset + dataset = ProofBank.load_data(settings.DATASET_PATH) diff --git a/backend/aethel_db/views.py b/backend/aethel_db/views.py index cab679d..a30a643 100644 --- a/backend/aethel_db/views.py +++ b/backend/aethel_db/views.py @@ -1,22 +1,23 @@ from dataclasses import asdict, dataclass, field from enum import Enum from typing import List, Optional + from django.http import HttpRequest, JsonResponse -from django.conf import settings from rest_framework import status from rest_framework.views import APIView -from aethel import ProofBank from aethel.frontend import LexicalItem from spindle.utils import serialize_phrases_with_infix_notation -from parseport.logger import logger from aethel_db.search import search, in_lemma, in_word from aethel.frontend import Sample -DATASET_PATH = getattr(settings, "DATASET_PATH") -try: - dataset = ProofBank.load_data(DATASET_PATH) -except FileNotFoundError: - logger.critical(f"Æthel dataset not found.") +from aethel.frontend import LexicalItem + +from .models import dataset +from .search import search, in_lemma, in_word + + +def aethel_status(): + return dataset is not None @dataclass diff --git a/backend/parseport/settings.py b/backend/parseport/settings.py index f1509fa..9e443de 100644 --- a/backend/parseport/settings.py +++ b/backend/parseport/settings.py @@ -124,7 +124,7 @@ SPINDLE_URL = f"http://pp-spindle:32768/" LATEX_SERVICE_URL = f"http://pp-latex:32769/" -DATA_SUBSET_PATH = "./aethel_db/data/aethel_subset.pickle" -FULL_DATASET_PATH = "./aethel.pickle" +data_subset_path = "./aethel_db/data/aethel_subset.pickle" +full_dataset_path = "./aethel.pickle" -DATASET_PATH = DATA_SUBSET_PATH if DEBUG else FULL_DATASET_PATH +DATASET_PATH = data_subset_path if DEBUG else full_dataset_path diff --git a/backend/parseport/urls.py b/backend/parseport/urls.py index 5344c84..51d6cea 100644 --- a/backend/parseport/urls.py +++ b/backend/parseport/urls.py @@ -20,6 +20,8 @@ from rest_framework import routers from spindle.views import SpindleView +from .views import StatusView + api_router = routers.DefaultRouter() # register viewsets with this router @@ -30,6 +32,7 @@ path("api-auth", RedirectView.as_view(url="/api-auth/", permanent=True)), path("admin/", admin.site.urls), path("api/", include(api_router.urls)), + path("api/status/", StatusView.as_view(), name="status"), path("api/spindle/", SpindleView.as_view(), name="spindle"), path("api/aethel/", include("aethel_db.urls")), path( diff --git a/backend/parseport/views.py b/backend/parseport/views.py new file mode 100644 index 0000000..411ad71 --- /dev/null +++ b/backend/parseport/views.py @@ -0,0 +1,13 @@ +from rest_framework.views import APIView +from rest_framework.response import Response + +from aethel_db.views import aethel_status +from spindle.views import spindle_status + + +class StatusView(APIView): + def get(self, request): + return Response(dict( + aethel=aethel_status(), + spindle=spindle_status() + )) diff --git a/backend/spindle/views.py b/backend/spindle/views.py index 2cede8b..5581dfe 100644 --- a/backend/spindle/views.py +++ b/backend/spindle/views.py @@ -204,3 +204,17 @@ def proof_response(self, parsed: ParserResponse) -> JsonResponse: return SpindleResponse( proof=serial_proof_to_json(serialize_proof(parsed.proof)) ).json_response() + + +def spindle_status(): + try: + r = http.request( + method="GET", + url=settings.SPINDLE_URL + '/status/', + headers={"Content-Type": "application/json"}, + timeout=1, + retries=False + ) + return r.status < 400 + except Exception: + return False diff --git a/frontend/src/app/aethel/aethel.component.html b/frontend/src/app/aethel/aethel.component.html index 999eaab..ad0c036 100644 --- a/frontend/src/app/aethel/aethel.component.html +++ b/frontend/src/app/aethel/aethel.component.html @@ -5,34 +5,41 @@

Æthel

start.

-
-
- -
- - +@if (status$ | async) { + +
+ +
+ + +
+ @if (form.touched && form.invalid) { +

+ Please enter at least three characters. +

+ }
- @if (form.touched && form.invalid) { -

- Please enter at least three characters. -

- } -
- + +} +@else { +

+ The Æthel dataset is temporarily unavailable. +

+} @if (submitted | async) {
diff --git a/frontend/src/app/aethel/aethel.component.ts b/frontend/src/app/aethel/aethel.component.ts index dc4466d..48aff28 100644 --- a/frontend/src/app/aethel/aethel.component.ts +++ b/frontend/src/app/aethel/aethel.component.ts @@ -3,13 +3,14 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { AethelListReturnItem } from "../shared/types"; import { AethelApiService } from "../shared/services/aethel-api.service"; -import { distinctUntilChanged, map } from "rxjs"; +import { Subject, distinctUntilChanged, map } from "rxjs"; import { faChevronDown, faChevronRight, } from "@fortawesome/free-solid-svg-icons"; import { ActivatedRoute, Router } from "@angular/router"; import { isNonNull } from "../shared/operators/IsNonNull"; +import { StatusService } from "../shared/services/status.service"; @Component({ selector: "pp-aethel", @@ -31,14 +32,19 @@ export class AethelComponent implements OnInit { chevronDown: faChevronDown, }; + status$ = new Subject(); + constructor( private apiService: AethelApiService, private destroyRef: DestroyRef, private router: Router, private route: ActivatedRoute, + private statusService: StatusService ) {} ngOnInit(): void { + this.statusService.get().subscribe(status => this.status$.next(status.aethel)); + this.apiService.output$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((response) => { diff --git a/frontend/src/app/shared/services/status.service.ts b/frontend/src/app/shared/services/status.service.ts new file mode 100644 index 0000000..a1164c5 --- /dev/null +++ b/frontend/src/app/shared/services/status.service.ts @@ -0,0 +1,20 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { environment } from "src/environments/environment"; + +interface Status { + aethel: boolean; + spindle: boolean; +} + +@Injectable({ + providedIn: "root" +}) +export class StatusService{ + constructor(private http: HttpClient) {} + + get(): Observable { + return this.http.get(`${environment.apiUrl}status/`); + } +} diff --git a/frontend/src/app/spindle/spindle.component.html b/frontend/src/app/spindle/spindle.component.html index 0e1bd0d..39ffc61 100644 --- a/frontend/src/app/spindle/spindle.component.html +++ b/frontend/src/app/spindle/spindle.component.html @@ -15,41 +15,50 @@

Spindle

References. The notations used are explained under Notation.

-
-
- - -

- This field is required. -

-
-
+ @if (spindleReady$ | async) { +
+
+ + +

+ This field is required. +

+
+
+ }
-
- -
+@if (spindleReady$ | async) { +
+ +
+} +@else { +

+ Spindle is temporarily unavailable. +

+}

Term:

diff --git a/frontend/src/app/spindle/spindle.component.ts b/frontend/src/app/spindle/spindle.component.ts index e36b9e5..d9366a4 100644 --- a/frontend/src/app/spindle/spindle.component.ts +++ b/frontend/src/app/spindle/spindle.component.ts @@ -7,6 +7,8 @@ import { AlertType } from "../shared/components/alert/alert.component"; import { faDownload, faCopy } from "@fortawesome/free-solid-svg-icons"; import { LexicalPhrase, SpindleMode } from "../shared/types"; import { SpindleApiService } from "../shared/services/spindle-api.service"; +import { Subject, filter, map, share, switchMap, takeUntil, timer } from "rxjs"; +import { StatusService } from "../shared/services/status.service"; interface TextOutput { extension: "json" | "tex"; @@ -30,14 +32,29 @@ export class SpindleComponent implements OnInit { faCopy = faCopy; faDownload = faDownload; + stopStatus$ = new Subject(); + + spindleReady$ = timer(0, 5000).pipe( + takeUntil(this.stopStatus$), + switchMap(() => this.statusService.get()), + map(status => status.spindle), + share() + ); + constructor( private apiService: SpindleApiService, private alertService: AlertService, private errorHandler: ErrorHandlerService, private destroyRef: DestroyRef, + private statusService: StatusService ) {} ngOnInit(): void { + this.spindleReady$.pipe( + filter(ready => ready === true), + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.stopStatus$.next()); + this.apiService.output$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((response) => {