Skip to content

Commit

Permalink
Merge branch 'develop' into feature/minimalist-parser
Browse files Browse the repository at this point in the history
  • Loading branch information
XanderVertegaal committed Dec 11, 2024
2 parents 3057461 + 312ad71 commit bc35c95
Show file tree
Hide file tree
Showing 51 changed files with 1,367 additions and 650 deletions.
27 changes: 21 additions & 6 deletions backend/aethel_db/views/detail.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from enum import Enum
from dataclasses import dataclass, asdict
from dataclasses import dataclass

from django.http import HttpRequest, JsonResponse
from rest_framework import status
from rest_framework.views import APIView
from spindle.utils import serialize_phrases_with_infix_notation
from spindle.utils import serialize_phrases
from aethel.frontend import Sample

from aethel_db.models import dataset
Expand All @@ -16,7 +16,23 @@ class AethelDetailResult:
name: str
term: str
subset: str
phrases: list[dict]
phrases: list[dict[str, str]]

def serialize(self):
return {
"sentence": self.sentence,
"name": self.name,
"term": self.term,
"subset": self.subset,
"phrases": [
{
"type": phrase["type"],
"displayType": phrase["display_type"],
"items": phrase["items"],
}
for phrase in self.phrases
],
}


class AethelDetailError(Enum):
Expand All @@ -43,18 +59,17 @@ def parse_sample(self, sample: Sample) -> None:
name=sample.name,
term=str(sample.proof.term),
subset=sample.subset,
phrases=serialize_phrases_with_infix_notation(sample.lexical_phrases),
phrases=serialize_phrases(sample.lexical_phrases),
)

def json_response(self) -> JsonResponse:
result = asdict(self.result) if self.result else None
status_code = (
AETHEL_DETAIL_STATUS_CODES[self.error] if self.error else status.HTTP_200_OK
)

return JsonResponse(
{
"result": result,
"result": self.result.serialize() if self.result else None,
"error": self.error,
},
status=status_code,
Expand Down
58 changes: 50 additions & 8 deletions backend/aethel_db/views/list.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import asdict, dataclass, field
from enum import Enum

from django.http import HttpRequest, JsonResponse
from rest_framework import status
Expand All @@ -8,7 +9,7 @@
from aethel_db.models import dataset

from aethel.frontend import LexicalPhrase
from aethel.mill.types import type_prefix, Type, type_repr
from aethel.mill.types import type_prefix, Type, type_repr, parse_prefix


@dataclass
Expand All @@ -22,6 +23,12 @@ class AethelListPhrase:
items: list[AethelListLexicalItem]


class AethelListError(Enum):
INVALID_LIMIT_OR_SKIP = "INVALID_LIMIT_OR_SKIP"
WORD_TOO_SHORT = "WORD_TOO_SHORT"
CANNOT_PARSE_TYPE = "CANNOT_PARSE_TYPE"


@dataclass
class AethelListResult:
phrase: AethelListPhrase
Expand All @@ -48,7 +55,9 @@ class AethelListResponse:
"""

results: dict[tuple[str, str, str], AethelListResult] = field(default_factory=dict)
error: str | None = None
error: AethelListError | None = None
limit: int = 10
skip: int = 0

def get_or_create_result(
self, phrase: LexicalPhrase, type: Type
Expand Down Expand Up @@ -82,11 +91,26 @@ def get_or_create_result(
return self.results.setdefault(key, new_result)

def json_response(self) -> JsonResponse:
results = [result.serialize() for result in self.results.values()]
if self.error:
return JsonResponse(
{
"results": [],
"totalCount": 0,
"error": self.error.value,
},
status=status.HTTP_400_BAD_REQUEST,
)

total_count = len(self.results)
limit = min(self.limit, total_count)
skip = max(0, self.skip)
paginated = list(self.results.values())[skip : skip + limit]
serialized = [result.serialize() for result in paginated]

return JsonResponse(
{
"results": results,
"results": serialized,
"totalCount": total_count,
"error": self.error,
},
status=status.HTTP_200_OK,
Expand All @@ -97,22 +121,40 @@ class AethelListView(APIView):
def get(self, request: HttpRequest) -> JsonResponse:
word_input = self.request.query_params.get("word", None)
type_input = self.request.query_params.get("type", None)
limit = self.request.query_params.get("limit", 10)
skip = self.request.query_params.get("skip", 0)

try:
limit = int(limit)
skip = int(skip)
except ValueError:
return AethelListResponse(
error=AethelListError.INVALID_LIMIT_OR_SKIP
).json_response()

# We only search for strings of 3 or more characters.
if word_input is not None and len(word_input) < 3:
return AethelListResponse().json_response()
return AethelListResponse(
error=AethelListError.WORD_TOO_SHORT
).json_response()

response_object = AethelListResponse(skip=skip, limit=limit)

try:
parsed_type = parse_prefix(type_input) if type_input else None
except Exception:
response_object.error = AethelListError.CANNOT_PARSE_TYPE
return response_object.json_response()

response_object = AethelListResponse()

for sample in dataset.samples:
for phrase in sample.lexical_phrases:
word_match = word_input and match_word_with_phrase(phrase, word_input)
type_match = type_input and match_type_with_phrase(phrase, type_input)
type_match = parsed_type and match_type_with_phrase(phrase, parsed_type)
if not (word_match or type_match):
continue

result = response_object.get_or_create_result(
# type_prefix returns a string representation of the type, with spaces between the elements.
phrase=phrase,
type=phrase.type,
)
Expand Down
45 changes: 35 additions & 10 deletions backend/aethel_db/views/sample_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import asdict, dataclass, field
from enum import Enum
import json
from django.http import HttpRequest, JsonResponse
from rest_framework.views import APIView
Expand All @@ -20,6 +21,11 @@ class AethelSampleDataPhrase:
highlight: bool


class AethelSampleError(Enum):
INVALID_WORD = "INVALID_WORD"
NO_INPUT = "NO_INPUT"


@dataclass
class AethelSampleDataResult:
name: str
Expand All @@ -35,7 +41,7 @@ def serialize(self):
@dataclass
class AethelSampleDataResponse:
results: list[AethelSampleDataResult] = field(default_factory=list)
error: str | None = None
error: AethelSampleError | None = None

def get_or_create_result(self, sample: Sample) -> AethelSampleDataResult:
"""
Expand All @@ -55,9 +61,10 @@ def get_or_create_result(self, sample: Sample) -> AethelSampleDataResult:
return new_result

def json_response(self) -> JsonResponse:
serialized = [result.serialize() for result in self.results]
return JsonResponse(
{
"results": [r.serialize() for r in self.results],
"results": serialized,
"error": self.error,
},
status=status.HTTP_200_OK,
Expand All @@ -71,27 +78,31 @@ def get(self, request: HttpRequest) -> JsonResponse:

response_object = AethelSampleDataResponse()

# Not expected.
if not type_input or not word_input:
error = self.validate_input(type_input, word_input)

if error:
response_object.error = error
return response_object.json_response()

word_input = json.loads(word_input)

assert dataset is not None
if dataset is None:
raise Exception("Dataset is not loaded.")

# parse_prefix expects a type string with spaces.
type_input = Type.parse_prefix(type_input, debug=True)
type_input = Type.parse_prefix(type_input)
by_type = dataset.by_type(str(type_input)) # re-serialize type to match index
by_word = dataset.by_words(word_input)
by_name = {sample.name: sample for sample in by_type + by_word}
# we have to do the intersection by name because Samples are not hashable
intersection = set(s.name for s in by_type).intersection(set(s.name for s in by_word))
intersection = set(s.name for s in by_type).intersection(
set(s.name for s in by_word)
)
samples = [by_name[name] for name in intersection]

for sample in samples:
for phrase_index, phrase in enumerate(sample.lexical_phrases):
word_match = match_word_with_phrase_exact(
phrase, word_input
)
word_match = match_word_with_phrase_exact(phrase, word_input)
type_match = match_type_with_phrase(phrase, type_input)

if not (word_match and type_match):
Expand All @@ -102,3 +113,17 @@ def get(self, request: HttpRequest) -> JsonResponse:
aethel_sample.highlight_phrase_at_index(index=phrase_index)

return response_object.json_response()

def validate_input(
self,
type_input: str | None,
word_input: str | None,
) -> AethelSampleError | None:
try:
word_input = json.loads(word_input)
except json.JSONDecodeError:
return AethelSampleError.INVALID_WORD

if not type_input or not word_input:
# Not expected.
return AethelSampleError.NO_INPUT
16 changes: 12 additions & 4 deletions backend/spindle/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
from aethel.frontend import LexicalPhrase
from aethel.mill.types import type_prefix, type_repr


def serialize_phrases_with_infix_notation(
def serialize_phrases(
lexical_phrases: list[LexicalPhrase],
) -> list[dict[str, str]]:
"""
Serializes a list of LexicalPhrases in a human-readable infix notation that is already available in Æthel in Type.__repr__.
Serializes a list of LexicalPhrases in a human-readable infix notation that is already available in Æthel in Type.__repr__ or type_repr(). This is used to display the types in the frontend.
The standard JSON serialization of phrases uses a prefix notation for types, which is good for data-exchange purposes (easier parsing) but less ideal for human consumption.
The standard JSON serialization of phrases uses a prefix notation for types, which is good for data-exchange purposes (easier parsing) but less ideal for human consumption. This notation is used to query.
"""
return [dict(phrase.json(), type=repr(phrase.type)) for phrase in lexical_phrases]
return [
dict(
phrase.json(),
display_type=type_repr(phrase.type),
type=type_prefix(phrase.type),
)
for phrase in lexical_phrases
]
14 changes: 9 additions & 5 deletions backend/spindle/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
serial_proof_to_json,
)

from spindle.utils import serialize_phrases_with_infix_notation
from spindle.utils import serialize_phrases

http = urllib3.PoolManager()

Expand All @@ -31,7 +31,6 @@
Mode = Literal["latex", "pdf", "overleaf", "term-table", "proof"]



class SpindleErrorSource(Enum):
INPUT = "input"
SPINDLE = "spindle"
Expand All @@ -52,6 +51,11 @@ class SpindleResponse:

def json_response(self) -> JsonResponse:
# TODO: set HTTP error code when error is not None

# Convert display_type to displayType for frontend.
for phrase in self.lexical_phrases:
phrase["displayType"] = phrase.pop("display_type")

return JsonResponse(
{
"latex": self.latex,
Expand Down Expand Up @@ -193,7 +197,7 @@ def overleaf_redirect(self, latex: str) -> JsonResponse:
def term_table_response(self, parsed: ParserResponse) -> JsonResponse:
"""Return the term and the lexical phrases as a JSON response."""

phrases = serialize_phrases_with_infix_notation(parsed.lexical_phrases)
phrases = serialize_phrases(parsed.lexical_phrases)
return SpindleResponse(
term=str(parsed.proof.term),
lexical_phrases=phrases,
Expand All @@ -210,10 +214,10 @@ def spindle_status():
try:
r = http.request(
method="GET",
url=settings.SPINDLE_URL + '/status/',
url=settings.SPINDLE_URL + "/status/",
headers={"Content-Type": "application/json"},
timeout=1,
retries=False
retries=False,
)
return r.status < 400
except Exception:
Expand Down
Loading

0 comments on commit bc35c95

Please sign in to comment.