Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add writing/reading history events #487

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions script/build_xquery_type_dicts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
20 changes: 20 additions & 0 deletions smoketest/smoketest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"], "<payload><kittens>1<cat/></kittens></payload>"
),
)
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
== '<payload xmlns:flag="http://caselaw.nationalarchives.gov.uk/history/flags" xmlns:prop="http://marklogic.com/xdmp/property"><kittens>1<cat/></kittens></payload>'
)
26 changes: 26 additions & 0 deletions src/caselawclient/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
55 changes: 55 additions & 0 deletions src/caselawclient/models/history.py
Original file line number Diff line number Diff line change
@@ -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 <payload> 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("<payload"):
raise EventPayloadInvalid("Event payloads must start with a <payload> 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 = "<payload/>"
return cls(attributes, flags, payload)
31 changes: 31 additions & 0 deletions src/caselawclient/xquery/append_history.xqy
Original file line number Diff line number Diff line change
@@ -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 := <payload><mice levels='3'>maurice</mice>kittens</payload>:)
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 := <event xmlns:flag="http://caselaw.nationalarchives.gov.uk/history/flags">
{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}
</event>

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, <history>{$event}</history>)
6 changes: 6 additions & 0 deletions src/caselawclient/xquery/get_history.xqy
Original file line number Diff line number Diff line change
@@ -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"))
13 changes: 13 additions & 0 deletions src/caselawclient/xquery_type_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down