diff --git a/script/build_xquery_type_dicts b/script/build_xquery_type_dicts index 19c57d04..3c253b83 100755 --- a/script/build_xquery_type_dicts +++ b/script/build_xquery_type_dicts @@ -28,6 +28,7 @@ DICTS_FILES_PATCH = join("src", "caselawclient", "xquery_type_dicts.py") ML_TYPES_TO_PYTHON_TYPES_DICT = { "xs:string": "str", "json:array": "list[Any]", + "json:object": "dict[Any, Any]", "xs:boolean": "bool", "xs:int": "int", } diff --git a/smoketest/smoketest.py b/smoketest/smoketest.py index c0466e31..8f1f77aa 100644 --- a/smoketest/smoketest.py +++ b/smoketest/smoketest.py @@ -5,6 +5,7 @@ import caselawclient.Client as Client from caselawclient.errors import DocumentNotFoundError from caselawclient.models.documents import Document +from caselawclient.models.history import HistoryEvent load_dotenv() env = environ.Env() @@ -43,3 +44,22 @@ def test_get_version_annotation(): api_client.get_version_annotation(FIRST_VERSION_URI) == "this is an annotation" ) assert Document(FIRST_VERSION_URI, api_client).annotation == "this is an annotation" + + +@pytest.mark.write +def test_append_history(): + api_client.append_history( + URI, + HistoryEvent( + {"id": "1"}, ["flag"], "1" + ), + ) + event = api_client.get_history(URI)[-1] + assert event.attributes["id"] == "1" + assert "datetime" in event.attributes + assert "flag" not in event.attributes + assert event.flags == ["flag"] + assert ( + event.payload + == '1' + ) diff --git a/src/caselawclient/Client.py b/src/caselawclient/Client.py index a6d008ea..ad44021c 100644 --- a/src/caselawclient/Client.py +++ b/src/caselawclient/Client.py @@ -11,6 +11,7 @@ from xml.etree.ElementTree import Element import environ +import lxml.etree import requests from requests.auth import HTTPBasicAuth from requests.structures import CaseInsensitiveDict @@ -24,6 +25,7 @@ Document, DocumentURIString, ) +from caselawclient.models.history import HistoryEvent from caselawclient.models.judgments import Judgment from caselawclient.models.press_summaries import PressSummary from caselawclient.models.utilities import move @@ -742,6 +744,7 @@ def original_judgment_transformation( ) def get_property(self, judgment_uri: DocumentURIString, name: str) -> str: + """This only gets the text of a property""" uri = self._format_uri_for_marklogic(judgment_uri) vars: query_dicts.GetPropertyDict = { "uri": uri, @@ -951,3 +954,26 @@ def get_combined_stats_table(self) -> list[list[Any]]: ) return results + + def append_history(self, uri: DocumentURIString, history: HistoryEvent) -> None: + formatted_uri = self._format_uri_for_marklogic(uri) + + vars: query_dicts.AppendHistoryDict = { + "uri": formatted_uri, + "attributes": history.attributes, + "flags": history.flags, + "payload": history.payload, + } + self._send_to_eval(vars, "append_history.xqy") + + def get_history(self, uri: DocumentURIString) -> list[HistoryEvent]: + formatted_uri = self._format_uri_for_marklogic(uri) + vars: query_dicts.GetHistoryDict = {"uri": formatted_uri} + response = self._send_to_eval(vars, "get_history.xqy") + bytes = get_single_bytestring_from_marklogic_response(response) + events = [ + HistoryEvent.from_xml(event) + for event in lxml.etree.fromstring(bytes).xpath("/history/event") + ] + breakpoint() + return events diff --git a/src/caselawclient/models/history.py b/src/caselawclient/models/history.py new file mode 100644 index 00000000..ab574bba --- /dev/null +++ b/src/caselawclient/models/history.py @@ -0,0 +1,55 @@ +from typing import Union + +import lxml.etree + +flag_ns_bare = "http://caselaw.nationalarchives.gov.uk/history/flags" +flag_ns_wrapped = f"{{{flag_ns_bare}}}" +namespaces = {"flag": flag_ns_bare} + + +class EventPayloadInvalid(RuntimeError): + """The payload did not start with a tag.""" + + pass + + +class HistoryEvent: + def __repr__(self) -> str: + return f"HistoryEvent({repr(self.attributes)}, {repr(self.flags)}, payload?)" + + def __init__( + self, + attributes: dict[str, str], + flags: list[str], + payload: Union[str, lxml.etree._Element], + ): + self.attributes = attributes + self.flags = flags + if isinstance(payload, lxml.etree._Element): + self.payload = lxml.etree.tostring(payload).decode("utf-8") + elif isinstance(payload, bytes): + self.payload = payload.decode("utf-8") + elif isinstance(payload, str): + self.payload = payload + + print(self.payload) + + if not self.payload.startswith(" tag") + + @classmethod + def from_xml(cls, element: lxml.etree._Element) -> "HistoryEvent": + raw_attribs = element.attrib + attributes = { + k: v for k, v in raw_attribs.items() if not k.startswith(flag_ns_wrapped) + } + flags = [ + k.partition(flag_ns_wrapped)[2] + for k in raw_attribs.keys() + if k.startswith(flag_ns_wrapped) + ] + try: + (payload,) = element.xpath("./payload") + except ValueError: # there might not be exactly 1 payload + payload = "" + return cls(attributes, flags, payload) diff --git a/src/caselawclient/xquery/append_history.xqy b/src/caselawclient/xquery/append_history.xqy new file mode 100644 index 00000000..4973c99d --- /dev/null +++ b/src/caselawclient/xquery/append_history.xqy @@ -0,0 +1,31 @@ +xquery version "1.0-ml"; + +import module namespace json="http://marklogic.com/xdmp/json" at "/MarkLogic/json/json.xqy"; +declare namespace basic="http://marklogic.com/xdmp/json/basic"; +declare namespace flag="http://caselaw.nationalarchives.gov.uk/history/flags"; + +(: let $attributes := json:transform-from-json(xdmp:unquote('{"id": "3", "type": "telemetry", "service": "ingester"}')) +let $flags := json:transform-from-json(xdmp:unquote('["failed", "automated"]')) +let $payload := mauricekittens:) +declare variable $uri as xs:string external; +declare variable $attributes as json:object external; +declare variable $flags as json:array external; +declare variable $payload as xs:string external; + +let $attributes-as-xml := json:transform-from-json($attributes) +let $flags-as-xml := json:transform-from-json($flags) +let $payload := xdmp:unquote($payload) + +let $event := + {for $i in $attributes-as-xml/* return attribute {$i/name()} {$i/text()} } + {attribute {"datetime"} {fn:current-dateTime()}} + {for $i in $flags-as-xml//basic:item return attribute {fn:QName("http://caselaw.nationalarchives.gov.uk/history/flags", $i/text())} {"true"}} + {$payload} + + +let $history := xdmp:document-get-properties($uri, xs:QName("history")) + +return if (fn:exists($history)) then + xdmp:node-insert-child($history, $event) +else + xdmp:document-set-property($uri, {$event}) diff --git a/src/caselawclient/xquery/get_history.xqy b/src/caselawclient/xquery/get_history.xqy new file mode 100644 index 00000000..6e3bd054 --- /dev/null +++ b/src/caselawclient/xquery/get_history.xqy @@ -0,0 +1,6 @@ +xquery version "1.0-ml"; + +import module namespace json="http://marklogic.com/xdmp/json" at "/MarkLogic/json/json.xqy"; +declare variable $uri as xs:string external; + +xdmp:document-get-properties($uri, xs:QName("history")) diff --git a/src/caselawclient/xquery_type_dicts.py b/src/caselawclient/xquery_type_dicts.py index 10547480..7a7a5bc5 100644 --- a/src/caselawclient/xquery_type_dicts.py +++ b/src/caselawclient/xquery_type_dicts.py @@ -17,6 +17,14 @@ class MarkLogicAPIDict(TypedDict): pass +# append_history.xqy +class AppendHistoryDict(MarkLogicAPIDict): + attributes: dict[Any, Any] + flags: list[Any] + payload: str + uri: MarkLogicDocumentURIString + + # break_judgment_checkout.xqy class BreakJudgmentCheckoutDict(MarkLogicAPIDict): uri: MarkLogicDocumentURIString @@ -55,6 +63,11 @@ class DocumentExistsDict(MarkLogicAPIDict): uri: MarkLogicDocumentURIString +# get_history.xqy +class GetHistoryDict(MarkLogicAPIDict): + uri: MarkLogicDocumentURIString + + # get_judgment.xqy class GetJudgmentDict(MarkLogicAPIDict): show_unpublished: Optional[bool]