ds-caselaw-marklogic-api-client 38.0.0__py3-none-any.whl → 39.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,7 @@ from caselawclient.models.utilities.aws import (
31
31
  request_parse,
32
32
  unpublish_documents,
33
33
  )
34
- from caselawclient.types import DocumentURIString
34
+ from caselawclient.types import DocumentURIString, SuccessFailureMessageTuple
35
35
 
36
36
  from .body import DocumentBody
37
37
  from .exceptions import CannotEnrichUnenrichableDocument, CannotPublishUnpublishableDocument, DocumentNotSafeForDeletion
@@ -521,9 +521,12 @@ class Document:
521
521
  """
522
522
  return self.body.has_content
523
523
 
524
+ def validate_identifiers(self) -> SuccessFailureMessageTuple:
525
+ return self.identifiers.perform_all_validations(document_type=type(self), api_client=self.api_client)
526
+
524
527
  def save_identifiers(self) -> None:
525
528
  """Validate the identifiers, and if the validation passes save them to MarkLogic"""
526
- validations = self.identifiers.perform_all_validations(document_type=type(self), api_client=self.api_client)
529
+ validations = self.validate_identifiers()
527
530
  if validations.success is True:
528
531
  self.api_client.set_property_as_node(self.uri, "identifiers", self.identifiers.as_etree)
529
532
  else:
@@ -570,7 +573,7 @@ class Document:
570
573
  def xml_with_correct_frbr(self) -> bytes:
571
574
  """Dynamically modify FRBR uris to reflect current storage location and FCL id"""
572
575
  fcl_identifiers = self.identifiers.of_type(FindCaseLawIdentifier)
573
- work_uri = f"https://caselaw.nationalarchives.gov.uk/id/{fcl_identifiers[0].url_slug}"
576
+ work_uri = f"https://caselaw.nationalarchives.gov.uk/id/doc/{fcl_identifiers[0].value}"
574
577
  expression_uri = f"https://caselaw.nationalarchives.gov.uk/{self.uri.lstrip('/')}"
575
578
  manifestation_uri = f"https://caselaw.nationalarchives.gov.uk/{self.uri.lstrip('/')}/data.xml"
576
579
  return self.body.apply_xslt(
@@ -58,7 +58,8 @@ class XML:
58
58
  """XSLT transform this XML, given a stylesheet"""
59
59
  passable_values = {k: etree.XSLT.strparam(v) for k, v in values.items()}
60
60
  xslt_transform = etree.XSLT(etree.fromstring(xslt))
61
- return etree.tostring(xslt_transform(self.xml_as_tree, profile_run=False, **passable_values))
61
+ noncanonical_xml = xslt_transform(self.xml_as_tree, profile_run=False, **passable_values)
62
+ return etree.tostring(noncanonical_xml, method="c14n2")
62
63
 
63
64
  def apply_xslt(self, xslt_filename: str, **values: str) -> bytes:
64
65
  """XSLT transform this XML, given a path to a stylesheet"""
@@ -149,6 +149,9 @@ class Identifier(ABC):
149
149
  @property
150
150
  def score(self) -> float:
151
151
  """Return the score of this identifier, used to calculate the preferred identifier for a document."""
152
+ if self.deprecated:
153
+ return 0
154
+
152
155
  return 1 * self.schema.base_score_multiplier
153
156
 
154
157
  def same_as(self, other: "Identifier") -> bool:
@@ -4,8 +4,7 @@ from lxml import etree
4
4
 
5
5
  from caselawclient.types import SuccessFailureMessageTuple
6
6
 
7
- from . import Identifier
8
- from .exceptions import UUIDMismatchError
7
+ from . import Identifier, IdentifierSchema
9
8
  from .fclid import FindCaseLawIdentifier
10
9
  from .neutral_citation import NeutralCitationNumber
11
10
  from .press_summary_ncn import PressSummaryRelatedNCNIdentifier
@@ -22,16 +21,61 @@ SUPPORTED_IDENTIFIER_TYPES: list[type["Identifier"]] = [
22
21
 
23
22
 
24
23
  class IdentifiersCollection(dict[str, Identifier]):
25
- def validate_uuids_match_keys(self) -> None:
24
+ def validate_uuids_match_keys(self) -> SuccessFailureMessageTuple:
26
25
  for uuid, identifier in self.items():
27
26
  if uuid != identifier.uuid:
28
- msg = "Key of {identifier} in Identifiers is {uuid} not {identifier.uuid}"
29
- raise UUIDMismatchError(msg)
27
+ return SuccessFailureMessageTuple(
28
+ False, [f"Key of {identifier} in Identifiers is {uuid} not {identifier.uuid}"]
29
+ )
30
30
 
31
- def perform_all_validations(
31
+ return SuccessFailureMessageTuple(True, [])
32
+
33
+ def _list_all_identifiers_by_schema(self) -> dict[type[IdentifierSchema], list[Identifier]]:
34
+ """Get a list of all identifiers, grouped by their schema."""
35
+ identifiers_by_schema: dict[type[IdentifierSchema], list[Identifier]] = {}
36
+
37
+ for identifier in self.values():
38
+ identifiers_by_schema.setdefault(identifier.schema, []).append(identifier)
39
+
40
+ return identifiers_by_schema
41
+
42
+ def check_only_single_non_deprecated_identifier_where_multiples_not_allowed(self) -> SuccessFailureMessageTuple:
43
+ """Check that only one non-deprecated identifier exists per schema where that schema does not allow multiples."""
44
+
45
+ for schema, identifiers in self._list_all_identifiers_by_schema().items():
46
+ non_deprecated_identifiers = [i for i in identifiers if not i.deprecated]
47
+ if len(non_deprecated_identifiers) > 1:
48
+ return SuccessFailureMessageTuple(
49
+ False,
50
+ [
51
+ f"Multiple non-deprecated identifiers found for schema '{schema.name}': {', '.join(i.value for i in non_deprecated_identifiers)}"
52
+ ],
53
+ )
54
+
55
+ return SuccessFailureMessageTuple(True, [])
56
+
57
+ def _perform_collection_level_validations(self) -> SuccessFailureMessageTuple:
58
+ """Perform identifier validations which are only possible at the collection level, such as UUID integrity and identifying exclusivity problems."""
59
+
60
+ success = True
61
+ messages: list[str] = []
62
+
63
+ collection_validations_to_run: list[SuccessFailureMessageTuple] = [
64
+ self.validate_uuids_match_keys(),
65
+ self.check_only_single_non_deprecated_identifier_where_multiples_not_allowed(),
66
+ ]
67
+
68
+ for validation in collection_validations_to_run:
69
+ if not validation.success:
70
+ success = False
71
+ messages += validation.messages
72
+
73
+ return SuccessFailureMessageTuple(success, messages)
74
+
75
+ def _perform_identifier_level_validations(
32
76
  self, document_type: type["Document"], api_client: "MarklogicApiClient"
33
77
  ) -> SuccessFailureMessageTuple:
34
- self.validate_uuids_match_keys()
78
+ """Perform identifier validations at the individual identifier level."""
35
79
 
36
80
  success = True
37
81
  messages: list[str] = []
@@ -45,14 +89,38 @@ class IdentifiersCollection(dict[str, Identifier]):
45
89
 
46
90
  return SuccessFailureMessageTuple(success, messages)
47
91
 
92
+ def perform_all_validations(
93
+ self, document_type: type["Document"], api_client: "MarklogicApiClient"
94
+ ) -> SuccessFailureMessageTuple:
95
+ """Perform all possible identifier validations on this collection, both at the individual and collection level."""
96
+
97
+ identifier_level_success, identifier_level_messages = self._perform_identifier_level_validations(
98
+ document_type=document_type, api_client=api_client
99
+ )
100
+ collection_level_success, collection_level_messages = self._perform_collection_level_validations()
101
+
102
+ success = all([identifier_level_success, collection_level_success])
103
+ all_messages = identifier_level_messages + collection_level_messages
104
+
105
+ return SuccessFailureMessageTuple(success, all_messages)
106
+
48
107
  def contains(self, other_identifier: Identifier) -> bool:
49
- "Do the identifier's value and namespace already exist in this group?"
108
+ """Does the identifier's value and namespace already exist in this group?"""
50
109
  return any(other_identifier.same_as(identifier) for identifier in self.values())
51
110
 
52
111
  def add(self, identifier: Identifier) -> None:
53
112
  if not self.contains(identifier):
54
113
  self[identifier.uuid] = identifier
55
114
 
115
+ def valid_new_identifier_types(self, document_type: type["Document"]) -> list[type[Identifier]]:
116
+ """Return a list of identifier types which can be added to a document of the given type, given identifiers already in this collection."""
117
+ return [
118
+ t
119
+ for t in SUPPORTED_IDENTIFIER_TYPES
120
+ if t.schema.allow_editing
121
+ and (not t.schema.document_types or document_type.__name__ in t.schema.document_types)
122
+ ]
123
+
56
124
  def __delitem__(self, key: Union[Identifier, str]) -> None:
57
125
  if isinstance(key, Identifier):
58
126
  super().__delitem__(key.uuid)
@@ -2,9 +2,5 @@ class InvalidIdentifierXMLRepresentationException(Exception):
2
2
  pass
3
3
 
4
4
 
5
- class UUIDMismatchError(Exception):
6
- pass
7
-
8
-
9
5
  class IdentifierValidationException(Exception):
10
6
  pass
@@ -1,6 +1,9 @@
1
1
  <?xml version="1.0"?>
2
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:uk='https://caselaw.nationalarchives.gov.uk/akn' xmlns:akn='http://docs.oasis-open.org/legaldocml/ns/akn/3.0'>
3
-
2
+ <xsl:stylesheet version="1.0"
3
+ xmlns='http://docs.oasis-open.org/legaldocml/ns/akn/3.0'
4
+ xmlns:akn='http://docs.oasis-open.org/legaldocml/ns/akn/3.0'
5
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
6
+ xmlns:uk='https://caselaw.nationalarchives.gov.uk/akn'>
4
7
  <xsl:param name="work_uri" />
5
8
  <xsl:param name="expression_uri" />
6
9
  <xsl:param name="manifestation_uri" />
@@ -16,51 +19,51 @@
16
19
  <!-- <xsl:template match="akn:identification/FRBRWork/FRBRthistext/text()"><xsl:copy-of select="$cat" /></xsl:template> -->
17
20
 
18
21
  <xsl:template match="akn:identification/akn:FRBRWork/akn:FRBRthis">
19
- <akn:FRBRthis>
22
+ <FRBRthis>
20
23
  <xsl:attribute name="value">
21
24
  <xsl:value-of select="$work_uri" />
22
25
  </xsl:attribute>
23
- </akn:FRBRthis>
26
+ </FRBRthis>
24
27
  </xsl:template>
25
28
 
26
29
  <xsl:template match="akn:identification/akn:FRBRWork/akn:FRBRuri">
27
- <akn:FRBRuri>
30
+ <FRBRuri>
28
31
  <xsl:attribute name="value">
29
32
  <xsl:value-of select="$work_uri" />
30
33
  </xsl:attribute>
31
- </akn:FRBRuri>
34
+ </FRBRuri>
32
35
  </xsl:template>
33
36
 
34
37
  <xsl:template match="akn:identification/akn:FRBRExpression/akn:FRBRthis">
35
- <akn:FRBRthis>
38
+ <FRBRthis>
36
39
  <xsl:attribute name="value">
37
40
  <xsl:value-of select="$expression_uri" />
38
41
  </xsl:attribute>
39
- </akn:FRBRthis>
42
+ </FRBRthis>
40
43
  </xsl:template>
41
44
 
42
45
  <xsl:template match="akn:identification/akn:FRBRExpression/akn:FRBRuri">
43
- <akn:FRBRuri>
46
+ <FRBRuri>
44
47
  <xsl:attribute name="value">
45
48
  <xsl:value-of select="$expression_uri" />
46
49
  </xsl:attribute>
47
- </akn:FRBRuri>
50
+ </FRBRuri>
48
51
  </xsl:template>
49
52
 
50
53
  <xsl:template match="akn:identification/akn:FRBRManifestation/akn:FRBRthis">
51
- <akn:FRBRthis>
54
+ <FRBRthis>
52
55
  <xsl:attribute name="value">
53
56
  <xsl:value-of select="$manifestation_uri" />
54
57
  </xsl:attribute>
55
- </akn:FRBRthis>
58
+ </FRBRthis>
56
59
  </xsl:template>
57
60
 
58
61
  <xsl:template match="akn:identification/akn:FRBRManifestation/akn:FRBRuri">
59
- <akn:FRBRuri>
62
+ <FRBRuri>
60
63
  <xsl:attribute name="value">
61
64
  <xsl:value-of select="$manifestation_uri" />
62
65
  </xsl:attribute>
63
- </akn:FRBRuri>
66
+ </FRBRuri>
64
67
  </xsl:template>
65
68
 
66
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ds-caselaw-marklogic-api-client
3
- Version: 38.0.0
3
+ Version: 39.0.0
4
4
  Summary: An API client for interacting with the underlying data in Find Caselaw.
5
5
  Keywords: national archives,caselaw
6
6
  Author: The National Archives
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Requires-Dist: boto3 (>=1.26.112,<2.0.0)
14
- Requires-Dist: certifi (>=2025.6.15,<2025.7.0)
14
+ Requires-Dist: certifi (>=2025.7.9,<2025.8.0)
15
15
  Requires-Dist: charset-normalizer (>=3.0.0,<4.0.0)
16
16
  Requires-Dist: django-environ (>=0.12.0)
17
17
  Requires-Dist: ds-caselaw-utils (>=2.0.0,<3.0.0)
@@ -7,15 +7,15 @@ caselawclient/errors.py,sha256=JC16fEGq_MRJX-_KFzfINCV2Cqx8o6OWOt3C16rQd84,3142
7
7
  caselawclient/factories.py,sha256=eGj9TiZpmF3todW-08Ps7bHNMvByHqwEbgujRhvU_Yc,7382
8
8
  caselawclient/identifier_resolution.py,sha256=B5I1sD7o7YjzsXMECjbKjgiGLDda5bGhejsJ-lYpTIg,2429
9
9
  caselawclient/models/__init__.py,sha256=kd23EUpvaC7aLHdgk8farqKAQEx3lf7RvNT2jEatvlg,68
10
- caselawclient/models/documents/__init__.py,sha256=3iEzgjX4ooLUKEuAeOylEUTm5rUQkrRztBmv4Rp1Ub4,21117
10
+ caselawclient/models/documents/__init__.py,sha256=0MYorde8Ku9qUWLa5G_aQUBKwDeTRZ_1ebsV9RxIBuI,21256
11
11
  caselawclient/models/documents/body.py,sha256=O1ZTV3KHo-YNi7Syd4oCV1CVSuRF7mcLXojwshyY4jg,6601
12
12
  caselawclient/models/documents/exceptions.py,sha256=te7PPQTDHjZ9EYVg5pVaiZfF00lMBFy333PHj8_mkC4,443
13
13
  caselawclient/models/documents/statuses.py,sha256=Cp4dTQmJOtsU41EJcxy5dV1841pGD2PNWH0VrkDEv4Q,579
14
14
  caselawclient/models/documents/transforms/html.xsl,sha256=XyUQLFcJ7_GwthWQ6ShU0bmzrgpl7xDFU-U8VLgOvEs,38258
15
- caselawclient/models/documents/xml.py,sha256=UPGL4Bm-RRde3ezkm1s1zcL-XdNqy1ncRcbsZNLtGn4,2135
16
- caselawclient/models/identifiers/__init__.py,sha256=IOMGUT4he1sZ7imjJupkyataB8OHMlsidoHFZJTWFsU,7581
17
- caselawclient/models/identifiers/collection.py,sha256=zcdFE831vYBh1dueULdKu3cdVEgUlNwCIwq6XNEC36M,4062
18
- caselawclient/models/identifiers/exceptions.py,sha256=ckVsjPzLuTXkbd7KZRJXoxcltQCXPGL2rMyYwE5orgg,177
15
+ caselawclient/models/documents/xml.py,sha256=BVra2VL_0JyImM8GC3wdouu1tApy79C-e2dHvQyrXPE,2195
16
+ caselawclient/models/identifiers/__init__.py,sha256=N2xcIOUlnG5w7yCzCAvGnlkN_rm6ceMHwA-CS1ZNoGk,7631
17
+ caselawclient/models/identifiers/collection.py,sha256=kGlziJiLAqoyd6LaaZ5tsgUf2fD6Y-7fv1It9S4-Otw,7448
18
+ caselawclient/models/identifiers/exceptions.py,sha256=6LVjvx-UOwqkrpxU19ydmrphKNw0rcG5GXwjTFyf8Dk,130
19
19
  caselawclient/models/identifiers/fclid.py,sha256=hj8z-VhXFrUHKOY6k_ItPvOakIvbhJ5xEbZ04E2j7t8,1521
20
20
  caselawclient/models/identifiers/neutral_citation.py,sha256=bYAeXHVm_ls0aDTeYI4uv35iZmJGSKU4-H-iLh2xED0,2912
21
21
  caselawclient/models/identifiers/press_summary_ncn.py,sha256=t-x6PsEe2tz1uO1qZKXKK0TugkQYb_49O_xgjd_oiE4,801
@@ -84,9 +84,9 @@ caselawclient/xquery/validate_document.xqy,sha256=PgaDcnqCRJPIVqfmWsNlXmCLNKd21q
84
84
  caselawclient/xquery/xslt.xqy,sha256=w57wNijH3dkwHkpKeAxqjlghVflQwo8cq6jS_sm-erM,199
85
85
  caselawclient/xquery/xslt_transform.xqy,sha256=cccaFiGkCcvSfDv007UriZ3I4ak2nTLP1trRZdbOoS8,2462
86
86
  caselawclient/xquery_type_dicts.py,sha256=zuyDGTkcN6voOXCm3APXItZ-Ey6tZ2hdZummZWzjl50,6489
87
- caselawclient/xslt/modify_xml_live.xsl,sha256=F-0FM0OhT9Ua1WVEVeHYwwVkktmTOERykOuGhMrKans,2346
87
+ caselawclient/xslt/modify_xml_live.xsl,sha256=gNjwBun2-UzOeeuf0wNjFtN3jXm1yrwqv_KT8r1slXw,2370
88
88
  caselawclient/xslt/sample.xsl,sha256=IG-v77stjwqiw25pguh391K-5DTKiX651WqILDZixm0,825
89
- ds_caselaw_marklogic_api_client-38.0.0.dist-info/LICENSE.md,sha256=fGMzyyLuQW-IAXUeDSCrRdsYW536aEWThdbpCjo6ZKg,1108
90
- ds_caselaw_marklogic_api_client-38.0.0.dist-info/METADATA,sha256=uvG9_FZesFrKHYJLzQpYw1JokKUIyrqAe7WPf1oVq2A,4320
91
- ds_caselaw_marklogic_api_client-38.0.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
92
- ds_caselaw_marklogic_api_client-38.0.0.dist-info/RECORD,,
89
+ ds_caselaw_marklogic_api_client-39.0.0.dist-info/LICENSE.md,sha256=fGMzyyLuQW-IAXUeDSCrRdsYW536aEWThdbpCjo6ZKg,1108
90
+ ds_caselaw_marklogic_api_client-39.0.0.dist-info/METADATA,sha256=hn5g5HHm3R3WIpBTeSKsv0IY6uVxKY6m7AkCo-pr_y4,4319
91
+ ds_caselaw_marklogic_api_client-39.0.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
92
+ ds_caselaw_marklogic_api_client-39.0.0.dist-info/RECORD,,