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]