From 3d23820d5293bb29b860c63a9b51513641a0bb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Cl=C3=A9rice?= Date: Mon, 26 Aug 2024 16:33:21 +0200 Subject: [PATCH] Navigation is now compatible with the ref/start/end/down table list --- dapitains/app/app.py | 68 ++++++++++++++++++++++++++----------- dapitains/app/navigation.py | 47 ++++++++++++++++--------- dapitains/errors.py | 3 ++ tests/test_db_create.py | 10 +++++- 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/dapitains/app/app.py b/dapitains/app/app.py index 59a3b99..8b1baf4 100644 --- a/dapitains/app/app.py +++ b/dapitains/app/app.py @@ -1,7 +1,5 @@ from typing import Dict, Any, Optional -from dapitains.tei.document import Document - try: import uritemplate from flask import Flask, request, Response @@ -13,8 +11,10 @@ import json import lxml.etree as ET +from dapitains.tei.document import Document +from dapitains.errors import InvalidRangeOrder from dapitains.app.database import db, Collection, Navigation -from dapitains.app.navigation import get_nav +from dapitains.app.navigation import get_nav, get_member_by_path def msg_4xx(string, code=404) -> Response: @@ -68,7 +68,7 @@ def collection_view( **( { "navigation": templates["navigation"].partial({"resource": related.identifier}).uri, - } if coll.citeStructure else {} + } if related.citeStructure else {} ) ) if related.resource else related.json({ "collection": templates["collection"].partial({"id": related.identifier}).uri @@ -124,7 +124,7 @@ def document_view(resource, ref, start, end, tree) -> Response: ) -def navigation_view(resource, ref, start, end, tree, down, templates: Dict[str, str]) -> Response: +def navigation_view(resource, ref, start, end, tree, down, templates: Dict[str, uritemplate.URITemplate]) -> Response: if not resource: return msg_4xx("Resource parameter was not provided") @@ -146,21 +146,51 @@ def navigation_view(resource, ref, start, end, tree, down, templates: Dict[str, return msg_4xx(f"You cannot provide a ref parameter as well as start or end", code=400) elif not ref and ((start and not end) or (end and not start)): return msg_4xx(f"Range is missing one of its parameters (start or end)", code=400) - else: - if down is None: - return msg_4xx(f"The down query parameter is required when requesting without ref or start/end", code=400) - refs = nav.references[tree] - paths = nav.paths[tree] - members, start, end = get_nav(refs=refs, paths=paths, start_or_ref=start or ref, end=end, down=down) - return Response(json.dumps({ + # Start the response + out = { "@context": "https://distributed-text-services.github.io/specifications/context/1-alpha1.json", "dtsVersion": "1-alpha", "@type": "Navigation", - "@id": "https://example.org/api/dts/navigation/?resource=https://en.wikisource.org/wiki/Dracula&down=1", - "resource": collection.json(inject=templates), # To Do: implement and inject URI templates - "member": members - }), mimetype="application/json", status=200) + "@id": templates["navigation"].expand({ + "ref": ref, "down": down, "start": start, "end": end, "tree": tree + }), + "resource": collection.json(inject={k:v.uri for k,v in templates.items()}), + } + + refs = nav.references[tree] + paths = nav.paths[tree] + + # Three first rows of the specs folr combination of down/ref/start/end + if down is None: + if ref: + out["ref"] = get_member_by_path(refs, paths[ref]) + elif start and end: + out["start"] = get_member_by_path(refs, paths[start]) + out["end"] = get_member_by_path(refs, paths[end]) + else: + return msg_4xx(f"The down query parameter is required when requesting without ref or start/end", code=400) + return Response(json.dumps(out), mimetype="application/json", status=200) + elif down == 0 and start and end: + return msg_4xx(f"The down query parameter cannot be `0` while using start/end", code=400) + elif down == 0 and not ref: + return msg_4xx(f"The down query parameter cannot be `0` without using the `ref` parameter", code=400) + + try: + members, start, end = get_nav(refs=refs, paths=paths, start_or_ref=start or ref, end=end, down=down) + except InvalidRangeOrder: + return msg_4xx("End reference comes before start in the document order. Interchange start and end.", code=400) + except Exception: + raise + + out["member"] = members + if end: + out["start"] = start + out["end"] = end + else: + out["ref"] = start + + return Response(json.dumps(out), mimetype="application/json", status=200) def create_app( @@ -212,9 +242,9 @@ def navigation_route(): down = request.args.get("down", type=int, default=None) return navigation_view(resource, ref, start, end, tree, down, templates={ - "navigation": navigation_template.partial({"resource": resource}).uri, - "collection": collection_template.partial({"id": resource}).uri, - "document": document_template.partial({"resource": resource}).uri, + "navigation": navigation_template.partial({"resource": resource}), + "collection": collection_template.partial({"id": resource}), + "document": document_template.partial({"resource": resource}), }) @app.route("/document/") diff --git a/dapitains/app/navigation.py b/dapitains/app/navigation.py index 0914847..adcc8a3 100644 --- a/dapitains/app/navigation.py +++ b/dapitains/app/navigation.py @@ -1,4 +1,5 @@ from typing import List, Dict, Any, Optional, Tuple +from dapitains.errors import InvalidRangeOrder def get_member_by_path(data: List[Dict[str, Any]], path: List[int]) -> Optional[Dict[str, Any]]: @@ -76,22 +77,37 @@ def get_nav( """ paths_index = list(paths.keys()) - start_index, end_index = None, None + start_index, end_index = None, len(paths_index) + if end: - end_index = paths_index.index(end) + 1 + # For end, as end is inclusive, we check for the last partial match + # (ie, if Mark is [1], we want everything starting + # by [1].) + end_index = paths_index.index(end) + len_end = len(paths[end]) + for idx, reference in enumerate(paths_index[end_index+1:]): + # print(paths[:len_end], paths[end]) + if paths[reference][:len_end] == paths[end]: + end_index = end_index+idx + else: + break + if start_or_ref: start_index = paths_index.index(start_or_ref) if not end: - for index, reference in enumerate(paths_index[start_index+1:]): - if len(paths[start_or_ref]) == len(paths[reference]): - end_index = index + start_index + 1 - - paths = dict(list(paths.items())[start_index:end_index]) - - current_level = [0] - + if down == 0: + end_index = len(paths_index) + else: + for index, reference in enumerate(paths_index[start_index+1:]): + if len(paths[start_or_ref]) == len(paths[reference]): + end_index = index + start_index + if start_index > end_index: + raise InvalidRangeOrder + + paths = dict(list(paths.items())[start_index:end_index+1]) + + current_level = [] start_path, end_path = None, None - if start_or_ref: start_path = paths[start_or_ref] current_level.append(len(start_path)) @@ -99,15 +115,14 @@ def get_nav( end_path = paths[end] current_level.append(len(end_path)) - current_level = max(current_level) - - if down == -1: - down = max(list(map(len, paths.values()))) + current_level = max(current_level) if current_level else 0 if down == 0: paths = {key: value for key, value in paths.items() if len(value) == current_level} + elif down == -1: + paths = {key: value for key, value in paths.items() if current_level <= len(value)} else: - paths = {key: value for key, value in paths.items() if current_level < len(value) <= down + current_level} + paths = {key: value for key, value in paths.items() if current_level <= len(value) <= down + current_level} return ( [ diff --git a/dapitains/errors.py b/dapitains/errors.py index 6d3b311..df53b8f 100644 --- a/dapitains/errors.py +++ b/dapitains/errors.py @@ -1,2 +1,5 @@ class UnknownTreeName(Exception): """This exception is raised when a requested tree is unknown """ + +class InvalidRangeOrder(Exception): + """Error raised when a range is in the wrong order (start > end) """ \ No newline at end of file diff --git a/tests/test_db_create.py b/tests/test_db_create.py index 72984f1..66936a6 100644 --- a/tests/test_db_create.py +++ b/tests/test_db_create.py @@ -61,7 +61,13 @@ def test_navigation(): } paths = {tree: generate_paths(ref) for tree, ref in refs.items()} - assert get_nav(refs[doc.default_tree], paths[doc.default_tree], start_or_ref=None, end=None, down=1) == ([ + assert get_nav( + refs[doc.default_tree], + paths[doc.default_tree], + start_or_ref=None, + end=None, + down=1 + ) == ([ {'citeType': 'book', 'ref': 'Luke', "level": 1, "parent": None}, {'citeType': 'book', 'ref': 'Mark', "level": 1, "parent": None} ], None, None), "Check that base function works" @@ -90,6 +96,7 @@ def test_navigation(): assert get_nav(refs[doc.default_tree], paths[doc.default_tree], start_or_ref="Luke 1", down=1) == ( [ + {'citeType': 'chapter', 'ref': 'Luke 1', "level": 2, "parent": "Luke"}, {'citeType': 'verse', 'ref': 'Luke 1:1', "level": 3, "parent": "Luke 1"}, {'citeType': 'verse', 'ref': 'Luke 1:2', "level": 3, "parent": "Luke 1"}, {'citeType': 'bloup', 'ref': 'Luke 1#1', "level": 3, "parent": "Luke 1"} @@ -100,6 +107,7 @@ def test_navigation(): assert get_nav(refs[doc.default_tree], paths[doc.default_tree], start_or_ref="Luke", down=1) == ( [ + {'citeType': 'book', 'ref': 'Luke', "level": 1, "parent": None}, {'citeType': 'chapter', 'ref': 'Luke 1', "level": 2, "parent": "Luke"}, ], {'citeType': 'book', 'ref': 'Luke', "level": 1, "parent": None},