ds-caselaw-marklogic-api-client 37.3.1__py3-none-any.whl → 38.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.
@@ -1,15 +1,14 @@
1
1
  import datetime
2
2
  import json
3
- from typing import Any, Optional
3
+ from typing import Any, Generic, Optional, Type, TypeAlias, TypeVar, cast
4
4
  from unittest.mock import Mock
5
5
 
6
- from typing_extensions import TypeAlias
7
-
8
6
  from caselawclient.Client import MarklogicApiClient
9
7
  from caselawclient.identifier_resolution import IdentifierResolution, IdentifierResolutions
10
8
  from caselawclient.models.documents import Document
11
9
  from caselawclient.models.documents.body import DocumentBody
12
10
  from caselawclient.models.identifiers import Identifier
11
+ from caselawclient.models.identifiers.collection import IdentifiersCollection
13
12
  from caselawclient.models.identifiers.fclid import FindCaseLawIdentifier
14
13
  from caselawclient.models.identifiers.neutral_citation import NeutralCitationNumber
15
14
  from caselawclient.models.judgments import Judgment
@@ -17,6 +16,8 @@ from caselawclient.models.press_summaries import PressSummary
17
16
  from caselawclient.responses.search_result import SearchResult, SearchResultMetadata
18
17
  from caselawclient.types import DocumentURIString
19
18
 
19
+ T = TypeVar("T")
20
+
20
21
  DEFAULT_DOCUMENT_BODY_XML = """<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:uk="https://caselaw.nationalarchives.gov.uk/akn">
21
22
  <judgment name="decision">
22
23
  <meta/><header/>
@@ -114,13 +115,13 @@ class PressSummaryFactory(DocumentFactory):
114
115
  }
115
116
 
116
117
 
117
- class SimpleFactory:
118
- target_class: TypeAlias = object
118
+ class SimpleFactory(Generic[T]):
119
+ target_class: Type[T]
119
120
  # "name_of_attribute": "default value"
120
121
  PARAMS_MAP: dict[str, Any]
121
122
 
122
123
  @classmethod
123
- def build(cls, **kwargs: Any) -> target_class:
124
+ def build(cls, **kwargs: Any) -> T:
124
125
  mock_object = Mock(spec=cls.target_class, autospec=True)
125
126
 
126
127
  for param, default in cls.PARAMS_MAP.items():
@@ -129,10 +130,10 @@ class SimpleFactory:
129
130
  else:
130
131
  setattr(mock_object.return_value, param, default)
131
132
 
132
- return mock_object()
133
+ return cast(T, mock_object())
133
134
 
134
135
 
135
- class SearchResultMetadataFactory(SimpleFactory):
136
+ class SearchResultMetadataFactory(SimpleFactory[SearchResultMetadata]):
136
137
  target_class = SearchResultMetadata
137
138
  # "name_of_attribute": "default value"
138
139
  PARAMS_MAP = {
@@ -174,9 +175,8 @@ class IdentifierResolutionsFactory:
174
175
  return IdentifierResolutions(resolutions)
175
176
 
176
177
 
177
- class SearchResultFactory(SimpleFactory):
178
+ class SearchResultFactory(SimpleFactory[SearchResult]):
178
179
  target_class = SearchResult
179
-
180
180
  PARAMS_MAP = {
181
181
  "uri": "d-a1b2c3",
182
182
  "name": "Judgment v Judgement",
@@ -189,5 +189,10 @@ class SearchResultFactory(SimpleFactory):
189
189
  "matches": None,
190
190
  "slug": "uksc/2025/1",
191
191
  "content_hash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
192
- "identifiers": {"id-1": NeutralCitationNumber("[2025] UKSC 123", "id-1")},
192
+ "identifiers": IdentifiersCollection(
193
+ {
194
+ "id-1": NeutralCitationNumber("[2025] UKSC 123", "id-1"),
195
+ "id-2": FindCaseLawIdentifier("bcdfghjk", "id-2"),
196
+ }
197
+ ),
193
198
  }
@@ -16,6 +16,7 @@ from caselawclient.errors import (
16
16
  )
17
17
  from caselawclient.identifier_resolution import IdentifierResolutions
18
18
  from caselawclient.models.identifiers import Identifier
19
+ from caselawclient.models.identifiers.exceptions import IdentifierValidationException
19
20
  from caselawclient.models.identifiers.fclid import FindCaseLawIdentifier, FindCaseLawIdentifierSchema
20
21
  from caselawclient.models.identifiers.unpacker import unpack_all_identifiers_from_etree
21
22
  from caselawclient.models.utilities import VersionsDict, extract_version, render_versions
@@ -33,7 +34,7 @@ from caselawclient.models.utilities.aws import (
33
34
  from caselawclient.types import DocumentURIString
34
35
 
35
36
  from .body import DocumentBody
36
- from .exceptions import CannotPublishUnpublishableDocument, DocumentNotSafeForDeletion
37
+ from .exceptions import CannotEnrichUnenrichableDocument, CannotPublishUnpublishableDocument, DocumentNotSafeForDeletion
37
38
  from .statuses import DOCUMENT_STATUS_HOLD, DOCUMENT_STATUS_IN_PROGRESS, DOCUMENT_STATUS_NEW, DOCUMENT_STATUS_PUBLISHED
38
39
 
39
40
  MINIMUM_ENRICHMENT_TIME = datetime.timedelta(minutes=20)
@@ -340,22 +341,34 @@ class Document:
340
341
  now.isoformat(),
341
342
  )
342
343
 
344
+ if not self.can_enrich:
345
+ msg = f"{self.uri} cannot be enriched"
346
+ raise CannotEnrichUnenrichableDocument(msg)
347
+
343
348
  announce_document_event(
344
349
  uri=self.uri,
345
350
  status="enrich",
346
351
  enrich=True,
347
352
  )
348
353
 
349
- def enrich(self) -> bool:
354
+ def enrich(self, even_if_recent: bool = False, accept_failures: bool = False) -> bool:
350
355
  """
351
356
  Request enrichment of a document, if it's sensible to do so.
352
357
  """
353
- if self.enriched_recently is False:
354
- print("Enrichment requested")
358
+ if not (even_if_recent) and self.enriched_recently:
359
+ print("Enrichment not requested as document was enriched recently")
360
+ return False
361
+
362
+ print("Enrichment requested")
363
+
364
+ try:
355
365
  self.force_enrich()
356
- return True
357
- print("Enrichment not requested as document was enriched recently")
358
- return False
366
+ except CannotEnrichUnenrichableDocument as e:
367
+ if not accept_failures:
368
+ raise e
369
+ return False
370
+
371
+ return True
359
372
 
360
373
  @cached_property
361
374
  def enriched_recently(self) -> bool:
@@ -501,10 +514,22 @@ class Document:
501
514
  """
502
515
  return self.docx_exists()
503
516
 
517
+ @cached_property
518
+ def can_enrich(self) -> bool:
519
+ """
520
+ Is it possible to enrich this document?
521
+ """
522
+ return self.body.has_content
523
+
504
524
  def save_identifiers(self) -> None:
505
- """Save the current state of this Document's identifiers to MarkLogic."""
506
- self.identifiers.validate()
507
- self.api_client.set_property_as_node(self.uri, "identifiers", self.identifiers.as_etree)
525
+ """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)
527
+ if validations.success is True:
528
+ self.api_client.set_property_as_node(self.uri, "identifiers", self.identifiers.as_etree)
529
+ else:
530
+ raise IdentifierValidationException(
531
+ "Unable to save identifiers; validation constraints not met: " + ", ".join(validations.messages)
532
+ )
508
533
 
509
534
  def __getattr__(self, name: str) -> Any:
510
535
  warnings.warn(f"{name} no longer exists on Document, using Document.body instead", DeprecationWarning)
@@ -541,3 +566,13 @@ class Document:
541
566
  def content_as_html(self) -> str | None:
542
567
  xlst_image_location = os.getenv("XSLT_IMAGE_LOCATION", "")
543
568
  return self.body.content_html(f"{xlst_image_location}/{self.uri}")
569
+
570
+ def xml_with_correct_frbr(self) -> bytes:
571
+ """Dynamically modify FRBR uris to reflect current storage location and FCL id"""
572
+ fcl_identifiers = self.identifiers.of_type(FindCaseLawIdentifier)
573
+ work_uri = f"https://caselaw.nationalarchives.gov.uk/id/{fcl_identifiers[0].url_slug}"
574
+ expression_uri = f"https://caselaw.nationalarchives.gov.uk/{self.uri.lstrip('/')}"
575
+ manifestation_uri = f"https://caselaw.nationalarchives.gov.uk/{self.uri.lstrip('/')}/data.xml"
576
+ return self.body.apply_xslt(
577
+ "modify_xml_live.xsl", work_uri=work_uri, expression_uri=expression_uri, manifestation_uri=manifestation_uri
578
+ )
@@ -51,6 +51,14 @@ class DocumentBody:
51
51
  def jurisdiction(self) -> str:
52
52
  return self.get_xpath_match_string("/akn:akomaNtoso/akn:*/akn:meta/akn:proprietary/uk:jurisdiction/text()")
53
53
 
54
+ @cached_property
55
+ def category(self) -> Optional[str]:
56
+ return self.get_xpath_match_string("/akn:akomaNtoso/akn:*/akn:meta/akn:proprietary/uk:category/text()")
57
+
58
+ @cached_property
59
+ def case_number(self) -> Optional[str]:
60
+ return self.get_xpath_match_string("/akn:akomaNtoso/akn:*/akn:meta/akn:proprietary/uk:caseNumber/text()")
61
+
54
62
  @property
55
63
  def court_and_jurisdiction_identifier_string(self) -> CourtCode:
56
64
  if self.jurisdiction != "":
@@ -166,3 +174,6 @@ class DocumentBody:
166
174
  :return: `True` if there was a complete parser failure, otherwise `False`
167
175
  """
168
176
  return "error" in self._xml.root_element
177
+
178
+ def apply_xslt(self, xslt_filename: str, **values: str) -> bytes:
179
+ return self._xml.apply_xslt(xslt_filename, **values)
@@ -2,5 +2,9 @@ class CannotPublishUnpublishableDocument(Exception):
2
2
  """A document which has failed publication safety checks in `Document.is_publishable` cannot be published."""
3
3
 
4
4
 
5
+ class CannotEnrichUnenrichableDocument(Exception):
6
+ """A document which cannot be enriched (see `Document.can_enrich`) tried to be sent to enrichment"""
7
+
8
+
5
9
  class DocumentNotSafeForDeletion(Exception):
6
10
  """A document which is not safe for deletion cannot be deleted."""
@@ -1,8 +1,16 @@
1
+ import os
2
+
1
3
  from lxml import etree
2
4
 
3
5
  from caselawclient.xml_helpers import get_xpath_match_string, get_xpath_match_strings
4
6
 
5
7
 
8
+ def _xslt_path(xslt_file_name: str) -> str:
9
+ from caselawclient.Client import ROOT_DIR
10
+
11
+ return os.path.join(ROOT_DIR, "xslt", xslt_file_name)
12
+
13
+
6
14
  class NonXMLDocumentError(Exception):
7
15
  """A document cannot be parsed as XML."""
8
16
 
@@ -41,3 +49,20 @@ class XML:
41
49
  namespaces: dict[str, str],
42
50
  ) -> list[str]:
43
51
  return get_xpath_match_strings(self.xml_as_tree, xpath, namespaces)
52
+
53
+ def _modified(
54
+ self,
55
+ xslt: str,
56
+ **values: str,
57
+ ) -> bytes:
58
+ """XSLT transform this XML, given a stylesheet"""
59
+ passable_values = {k: etree.XSLT.strparam(v) for k, v in values.items()}
60
+ xslt_transform = etree.XSLT(etree.fromstring(xslt))
61
+ return etree.tostring(xslt_transform(self.xml_as_tree, profile_run=False, **passable_values))
62
+
63
+ def apply_xslt(self, xslt_filename: str, **values: str) -> bytes:
64
+ """XSLT transform this XML, given a path to a stylesheet"""
65
+ full_xslt_filename = _xslt_path(xslt_filename)
66
+ with open(full_xslt_filename) as f:
67
+ xslt = f.read()
68
+ return self._modified(xslt, **values)
@@ -1,23 +1,31 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Optional, Union
2
+ from typing import TYPE_CHECKING, Any, Optional
3
3
  from uuid import uuid4
4
4
 
5
5
  from lxml import etree
6
6
 
7
- from caselawclient.types import DocumentIdentifierSlug, DocumentIdentifierValue
7
+ from caselawclient.types import DocumentIdentifierSlug, DocumentIdentifierValue, SuccessFailureMessageTuple
8
8
 
9
- from .exceptions import IdentifierValidationException, UUIDMismatchError
9
+ from .exceptions import IdentifierValidationException
10
+
11
+ if TYPE_CHECKING:
12
+ from caselawclient.Client import MarklogicApiClient
13
+ from caselawclient.models.documents import Document
10
14
 
11
15
  IDENTIFIER_PACKABLE_ATTRIBUTES: list[str] = [
12
16
  "uuid",
13
17
  "value",
18
+ "deprecated",
14
19
  "url_slug",
15
20
  ]
21
+ """A list of attributes of an Identifier to pack into an XML representation."""
16
22
 
17
23
  IDENTIFIER_UNPACKABLE_ATTRIBUTES: list[str] = [
18
24
  "uuid",
19
25
  "value",
26
+ "deprecated",
20
27
  ]
28
+ """A list of attributes to unpack from an XML representation."""
21
29
 
22
30
 
23
31
  class IdentifierSchema(ABC):
@@ -34,6 +42,19 @@ class IdentifierSchema(ABC):
34
42
  base_score_multiplier: float = 1.0
35
43
  """ A multiplier used to adjust the relative ranking of this identifier when calculating preferred identifiers. """
36
44
 
45
+ allow_editing: bool = True
46
+ """ Should editors be allowed to manually manipulate identifiers under this schema? """
47
+
48
+ require_globally_unique: bool = True
49
+ """ Must this identifier be globally unique? """
50
+
51
+ document_types: Optional[list[str]] = None
52
+ """
53
+ If present, a list of the names of document classes which can have this identifier.
54
+
55
+ If `None`, this identifier is valid for all document types.
56
+ """
57
+
37
58
  def __init_subclass__(cls: type["IdentifierSchema"], **kwargs: Any) -> None:
38
59
  """Ensure that subclasses have the required attributes set."""
39
60
  for required in (
@@ -50,7 +71,7 @@ class IdentifierSchema(ABC):
50
71
 
51
72
  @classmethod
52
73
  @abstractmethod
53
- def validate_identifier(cls, value: str) -> bool:
74
+ def validate_identifier_value(cls, value: str) -> bool:
54
75
  """Check that any given identifier value is valid for this schema."""
55
76
  pass
56
77
 
@@ -69,6 +90,9 @@ class Identifier(ABC):
69
90
  uuid: str
70
91
  value: DocumentIdentifierValue
71
92
 
93
+ deprecated: bool
94
+ """Should this identifier be considered deprecated, ie although we know it refers to a particular document its usage should be discouraged?"""
95
+
72
96
  def __init_subclass__(cls: type["Identifier"], **kwargs: Any) -> None:
73
97
  """Ensure that subclasses have the required attributes set."""
74
98
  for required in ("schema",):
@@ -77,13 +101,17 @@ class Identifier(ABC):
77
101
  super().__init_subclass__(**kwargs)
78
102
 
79
103
  def __repr__(self) -> str:
80
- return f"<{self.schema.name} {self.value}: {self.uuid}>"
104
+ representation = f"{self.schema.name} {self.value}: {self.uuid}"
105
+
106
+ if self.deprecated:
107
+ return f"<{representation} (deprecated)> "
108
+ return f"<{representation}>"
81
109
 
82
110
  def __str__(self) -> str:
83
111
  return self.value
84
112
 
85
- def __init__(self, value: str, uuid: Optional[str] = None) -> None:
86
- if not self.schema.validate_identifier(value=value):
113
+ def __init__(self, value: str, uuid: Optional[str] = None, deprecated: bool = False) -> None:
114
+ if not self.schema.validate_identifier_value(value=value):
87
115
  raise IdentifierValidationException(
88
116
  f'Identifier value "{value}" is not valid according to the {self.schema.name} schema.'
89
117
  )
@@ -94,6 +122,8 @@ class Identifier(ABC):
94
122
  else:
95
123
  self.uuid = "id-" + str(uuid4())
96
124
 
125
+ self.deprecated = deprecated
126
+
97
127
  @property
98
128
  def as_xml_tree(self) -> etree._Element:
99
129
  """Convert this Identifier into a packed XML representation for storage."""
@@ -102,9 +132,13 @@ class Identifier(ABC):
102
132
  namespace_attribute = etree.SubElement(identifier_root, "namespace")
103
133
  namespace_attribute.text = self.schema.namespace
104
134
 
105
- for attribute in IDENTIFIER_PACKABLE_ATTRIBUTES:
106
- packed_attribute = etree.SubElement(identifier_root, attribute)
107
- packed_attribute.text = getattr(self, attribute)
135
+ for attribute_name in IDENTIFIER_PACKABLE_ATTRIBUTES:
136
+ packed_attribute = etree.SubElement(identifier_root, attribute_name)
137
+ attribute_value = getattr(self, attribute_name)
138
+ if type(attribute_value) is bool:
139
+ packed_attribute.text = str(attribute_value).lower()
140
+ else:
141
+ packed_attribute.text = getattr(self, attribute_name)
108
142
 
109
143
  return identifier_root
110
144
 
@@ -121,66 +155,54 @@ class Identifier(ABC):
121
155
  "Is this the same as another identifier (in value and schema)?"
122
156
  return self.value == other.value and self.schema == other.schema
123
157
 
158
+ def validate_require_globally_unique(self, api_client: "MarklogicApiClient") -> SuccessFailureMessageTuple:
159
+ """
160
+ Check against the list of identifiers in the database that this value does not currently exist.
124
161
 
125
- class Identifiers(dict[str, Identifier]):
126
- def validate(self) -> None:
127
- for uuid, identifier in self.items():
128
- if uuid != identifier.uuid:
129
- msg = "Key of {identifier} in Identifiers is {uuid} not {identifier.uuid}"
130
- raise UUIDMismatchError(msg)
131
-
132
- def contains(self, other_identifier: Identifier) -> bool:
133
- "Do the identifier's value and namespace already exist in this group?"
134
- return any(other_identifier.same_as(identifier) for identifier in self.values())
135
-
136
- def add(self, identifier: Identifier) -> None:
137
- if not self.contains(identifier):
138
- self[identifier.uuid] = identifier
139
-
140
- def __delitem__(self, key: Union[Identifier, str]) -> None:
141
- if isinstance(key, Identifier):
142
- super().__delitem__(key.uuid)
143
- else:
144
- super().__delitem__(key)
162
+ nb: We don't need to check that the identifier value is unique within a parent `Identifiers` object, because `Identifiers.add()` will only allow one value per namespace.
163
+ """
164
+ resolutions = [
165
+ resolution
166
+ for resolution in api_client.resolve_from_identifier_value(identifier_value=self.value)
167
+ if resolution.identifier_namespace == self.schema.namespace
168
+ ]
169
+ if len(resolutions) > 0:
170
+ return SuccessFailureMessageTuple(
171
+ False,
172
+ [f'Identifiers in scheme "{self.schema.namespace}" must be unique; "{self.value}" already exists!'],
173
+ )
145
174
 
146
- def of_type(self, identifier_type: type[Identifier]) -> list[Identifier]:
147
- """Return a list of all identifiers of a given type."""
148
- uuids = self.keys()
149
- return [self[uuid] for uuid in list(uuids) if isinstance(self[uuid], identifier_type)]
175
+ return SuccessFailureMessageTuple(True, [])
150
176
 
151
- def delete_type(self, deleted_identifier_type: type[Identifier]) -> None:
152
- "For when we want an identifier to be the only valid identifier of that type, delete the others first"
153
- uuids = self.keys()
154
- for uuid in list(uuids):
155
- # we could use compare to .schema instead, which would have diffferent behaviour for subclasses
156
- if isinstance(self[uuid], deleted_identifier_type):
157
- del self[uuid]
177
+ def validate_valid_for_document_type(self, document_type: type["Document"]) -> SuccessFailureMessageTuple:
178
+ document_type_classname = document_type.__name__
158
179
 
159
- @property
160
- def as_etree(self) -> etree._Element:
161
- """Return an etree representation of all the Document's identifiers."""
162
- identifiers_root = etree.Element("identifiers")
180
+ if self.schema.document_types and document_type_classname not in self.schema.document_types:
181
+ return SuccessFailureMessageTuple(
182
+ False,
183
+ [
184
+ f'Document type "{document_type_classname}" is not accepted for identifier schema "{self.schema.name}"'
185
+ ],
186
+ )
163
187
 
164
- for identifier in self.values():
165
- identifiers_root.append(identifier.as_xml_tree)
188
+ return SuccessFailureMessageTuple(True, [])
166
189
 
167
- return identifiers_root
190
+ def perform_all_validations(
191
+ self, document_type: type["Document"], api_client: "MarklogicApiClient"
192
+ ) -> SuccessFailureMessageTuple:
193
+ """Perform all validations on a given identifier"""
194
+ validations = [
195
+ self.validate_require_globally_unique(api_client=api_client),
196
+ self.validate_valid_for_document_type(document_type=document_type),
197
+ ]
168
198
 
169
- def by_score(self, type: Optional[type[Identifier]] = None) -> list[Identifier]:
170
- """
171
- :param type: Optionally, an identifier type to constrain this list to.
199
+ success = True
200
+ messages: list[str] = []
172
201
 
173
- :return: Return a list of identifiers, sorted by their score in descending order.
174
- """
175
- identifiers = self.of_type(type) if type else list(self.values())
176
- return sorted(identifiers, key=lambda v: v.score, reverse=True)
202
+ for validation in validations:
203
+ if validation.success is False:
204
+ success = False
177
205
 
178
- def preferred(self, type: Optional[type[Identifier]] = None) -> Optional[Identifier]:
179
- """
180
- :param type: Optionally, an identifier type to constrain the results to.
206
+ messages += validation.messages
181
207
 
182
- :return: Return the highest scoring identifier of the given type (or of any type, if none is specified). Returns `None` if no identifier is available.
183
- """
184
- if len(self.by_score(type)) == 0:
185
- return None
186
- return self.by_score(type)[0]
208
+ return SuccessFailureMessageTuple(success, messages)
@@ -0,0 +1,102 @@
1
+ from typing import TYPE_CHECKING, Optional, Union
2
+
3
+ from lxml import etree
4
+
5
+ from caselawclient.types import SuccessFailureMessageTuple
6
+
7
+ from . import Identifier
8
+ from .exceptions import UUIDMismatchError
9
+ from .fclid import FindCaseLawIdentifier
10
+ from .neutral_citation import NeutralCitationNumber
11
+ from .press_summary_ncn import PressSummaryRelatedNCNIdentifier
12
+
13
+ if TYPE_CHECKING:
14
+ from caselawclient.Client import MarklogicApiClient
15
+ from caselawclient.models.documents import Document
16
+
17
+ SUPPORTED_IDENTIFIER_TYPES: list[type["Identifier"]] = [
18
+ FindCaseLawIdentifier,
19
+ NeutralCitationNumber,
20
+ PressSummaryRelatedNCNIdentifier,
21
+ ]
22
+
23
+
24
+ class IdentifiersCollection(dict[str, Identifier]):
25
+ def validate_uuids_match_keys(self) -> None:
26
+ for uuid, identifier in self.items():
27
+ if uuid != identifier.uuid:
28
+ msg = "Key of {identifier} in Identifiers is {uuid} not {identifier.uuid}"
29
+ raise UUIDMismatchError(msg)
30
+
31
+ def perform_all_validations(
32
+ self, document_type: type["Document"], api_client: "MarklogicApiClient"
33
+ ) -> SuccessFailureMessageTuple:
34
+ self.validate_uuids_match_keys()
35
+
36
+ success = True
37
+ messages: list[str] = []
38
+
39
+ for _, identifier in self.items():
40
+ validations = identifier.perform_all_validations(document_type=document_type, api_client=api_client)
41
+ if validations.success is False:
42
+ success = False
43
+
44
+ messages += validations.messages
45
+
46
+ return SuccessFailureMessageTuple(success, messages)
47
+
48
+ def contains(self, other_identifier: Identifier) -> bool:
49
+ "Do the identifier's value and namespace already exist in this group?"
50
+ return any(other_identifier.same_as(identifier) for identifier in self.values())
51
+
52
+ def add(self, identifier: Identifier) -> None:
53
+ if not self.contains(identifier):
54
+ self[identifier.uuid] = identifier
55
+
56
+ def __delitem__(self, key: Union[Identifier, str]) -> None:
57
+ if isinstance(key, Identifier):
58
+ super().__delitem__(key.uuid)
59
+ else:
60
+ super().__delitem__(key)
61
+
62
+ def of_type(self, identifier_type: type[Identifier]) -> list[Identifier]:
63
+ """Return a list of all identifiers of a given type."""
64
+ uuids = self.keys()
65
+ return [self[uuid] for uuid in list(uuids) if isinstance(self[uuid], identifier_type)]
66
+
67
+ def delete_type(self, deleted_identifier_type: type[Identifier]) -> None:
68
+ "For when we want an identifier to be the only valid identifier of that type, delete the others first"
69
+ uuids = self.keys()
70
+ for uuid in list(uuids):
71
+ # we could use compare to .schema instead, which would have diffferent behaviour for subclasses
72
+ if isinstance(self[uuid], deleted_identifier_type):
73
+ del self[uuid]
74
+
75
+ @property
76
+ def as_etree(self) -> etree._Element:
77
+ """Return an etree representation of all the Document's identifiers."""
78
+ identifiers_root = etree.Element("identifiers")
79
+
80
+ for identifier in self.values():
81
+ identifiers_root.append(identifier.as_xml_tree)
82
+
83
+ return identifiers_root
84
+
85
+ def by_score(self, type: Optional[type[Identifier]] = None) -> list[Identifier]:
86
+ """
87
+ :param type: Optionally, an identifier type to constrain this list to.
88
+
89
+ :return: Return a list of identifiers, sorted by their score in descending order.
90
+ """
91
+ identifiers = self.of_type(type) if type else list(self.values())
92
+ return sorted(identifiers, key=lambda v: v.score, reverse=True)
93
+
94
+ def preferred(self, type: Optional[type[Identifier]] = None) -> Optional[Identifier]:
95
+ """
96
+ :param type: Optionally, an identifier type to constrain the results to.
97
+
98
+ :return: Return the highest scoring identifier of the given type (or of any type, if none is specified). Returns `None` if no identifier is available.
99
+ """
100
+ if len(self.by_score(type)) == 0:
101
+ return None
102
+ return self.by_score(type)[0]
@@ -32,8 +32,11 @@ class FindCaseLawIdentifierSchema(IdentifierSchema):
32
32
  human_readable = False
33
33
  base_score_multiplier = 0.6
34
34
 
35
+ allow_editing = False
36
+ require_globally_unique = True
37
+
35
38
  @classmethod
36
- def validate_identifier(cls, value: str) -> bool:
39
+ def validate_identifier_value(cls, value: str) -> bool:
37
40
  return bool(VALID_FCLID_PATTERN.match(value))
38
41
 
39
42
  @classmethod
@@ -48,8 +48,10 @@ class NeutralCitationNumberSchema(IdentifierSchema):
48
48
  human_readable = True
49
49
  base_score_multiplier = 1.5
50
50
 
51
+ document_types = ["Judgment"]
52
+
51
53
  @classmethod
52
- def validate_identifier(cls, value: str) -> bool:
54
+ def validate_identifier_value(cls, value: str) -> bool:
53
55
  # Quick check to see if the NCN matches the expected pattern
54
56
  if not bool(VALID_NCN_PATTERN.match(value)):
55
57
  raise NCNDoesNotMatchExpectedPatternException(f"NCN '{value}' is not in the expected format")
@@ -13,6 +13,8 @@ class PressSummaryRelatedNCNIdentifierSchema(NeutralCitationNumberSchema):
13
13
  human_readable = True
14
14
  base_score_multiplier = 0.8
15
15
 
16
+ document_types = ["PressSummary"]
17
+
16
18
  @classmethod
17
19
  def compile_identifier_url_slug(cls, value: str) -> DocumentIdentifierSlug:
18
20
  return DocumentIdentifierSlug(super().compile_identifier_url_slug(value) + "/press-summary")
@@ -3,22 +3,18 @@ from warnings import warn
3
3
 
4
4
  from lxml import etree
5
5
 
6
- from . import IDENTIFIER_UNPACKABLE_ATTRIBUTES, Identifier, Identifiers
6
+ from . import IDENTIFIER_UNPACKABLE_ATTRIBUTES, Identifier
7
+ from .collection import SUPPORTED_IDENTIFIER_TYPES, IdentifiersCollection
7
8
  from .exceptions import InvalidIdentifierXMLRepresentationException
8
- from .fclid import FindCaseLawIdentifier
9
- from .neutral_citation import NeutralCitationNumber
10
- from .press_summary_ncn import PressSummaryRelatedNCNIdentifier
11
9
 
12
10
  IDENTIFIER_NAMESPACE_MAP: dict[str, type[Identifier]] = {
13
- "fclid": FindCaseLawIdentifier,
14
- "ukncn": NeutralCitationNumber,
15
- "uksummaryofncn": PressSummaryRelatedNCNIdentifier,
11
+ identifier_type.schema.namespace: identifier_type for identifier_type in SUPPORTED_IDENTIFIER_TYPES
16
12
  }
17
13
 
18
14
 
19
- def unpack_all_identifiers_from_etree(identifiers_etree: Optional[etree._Element]) -> Identifiers:
15
+ def unpack_all_identifiers_from_etree(identifiers_etree: Optional[etree._Element]) -> IdentifiersCollection:
20
16
  """This expects the entire <identifiers> tag, and unpacks all Identifiers inside it"""
21
- identifiers = Identifiers()
17
+ identifiers = IdentifiersCollection()
22
18
  if identifiers_etree is None:
23
19
  return identifiers
24
20
  for identifier_etree in identifiers_etree.findall("identifier"):
@@ -43,14 +39,23 @@ def unpack_an_identifier_from_etree(identifier_xml: etree._Element) -> Optional[
43
39
  warn(f"Identifier type {namespace_element.text} is not known.")
44
40
  return None
45
41
 
46
- kwargs: dict[str, str] = {}
42
+ str_kwargs: dict[str, str] = {}
43
+ deprecated = False
47
44
 
48
45
  for attribute in IDENTIFIER_UNPACKABLE_ATTRIBUTES:
49
46
  element = identifier_xml.find(attribute)
50
- if element is None or not element.text:
51
- raise InvalidIdentifierXMLRepresentationException(
52
- f"Identifer XML representation is not valid: {element} not present or empty"
53
- )
54
- kwargs[attribute] = element.text
55
47
 
56
- return IDENTIFIER_NAMESPACE_MAP[namespace_element.text](**kwargs)
48
+ # Special case for unpacking deprecation state into a boolean
49
+ if attribute == "deprecated":
50
+ if element is not None and element.text is not None and element.text.lower() == "true":
51
+ deprecated = True
52
+
53
+ else:
54
+ # Case for unpacking all other element types
55
+ if element is None or not element.text:
56
+ raise InvalidIdentifierXMLRepresentationException(
57
+ f"Identifer XML representation is not valid: {element} not present or empty"
58
+ )
59
+ str_kwargs[attribute] = element.text
60
+
61
+ return IDENTIFIER_NAMESPACE_MAP[namespace_element.text](deprecated=deprecated, **str_kwargs)
@@ -12,7 +12,7 @@ from ds_caselaw_utils.types import CourtCode, JurisdictionCode
12
12
  from lxml import etree
13
13
 
14
14
  from caselawclient.Client import MarklogicApiClient
15
- from caselawclient.models.identifiers import Identifiers
15
+ from caselawclient.models.identifiers.collection import IdentifiersCollection
16
16
  from caselawclient.models.identifiers.unpacker import unpack_all_identifiers_from_etree
17
17
  from caselawclient.types import DocumentURIString
18
18
  from caselawclient.xml_helpers import get_xpath_match_string
@@ -180,7 +180,7 @@ class SearchResult:
180
180
  )
181
181
 
182
182
  @property
183
- def identifiers(self) -> Identifiers:
183
+ def identifiers(self) -> IdentifiersCollection:
184
184
  identifiers_etrees = self._get_xpath(".//identifiers")
185
185
  count = len(identifiers_etrees)
186
186
  if count != 1:
caselawclient/types.py CHANGED
@@ -1,3 +1,6 @@
1
+ from typing import NamedTuple
2
+
3
+
1
4
  class InvalidDocumentURIException(Exception):
2
5
  """The document URI is not valid."""
3
6
 
@@ -58,3 +61,11 @@ class DocumentIdentifierSlug(str):
58
61
 
59
62
  class DocumentIdentifierValue(str):
60
63
  pass
64
+
65
+
66
+ SuccessFailureMessageTuple = NamedTuple("SuccessFailureMessageTuple", [("success", bool), ("messages", list[str])])
67
+ """
68
+ A tuple used to return if an operation has succeeded or failed (and optionally a list of messages associated with that operation).
69
+
70
+ This should only be used where a failure is considered a routine part of the application (eg during validation options); where an unexpected action has led to a failure the application should raise an appropriate exception.
71
+ """
@@ -0,0 +1,67 @@
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
+
4
+ <xsl:param name="work_uri" />
5
+ <xsl:param name="expression_uri" />
6
+ <xsl:param name="manifestation_uri" />
7
+ <xsl:output method="xml" indent="yes" />
8
+
9
+ <!-- Identify transformation -->
10
+ <xsl:template match="@* | node()">
11
+ <xsl:copy>
12
+ <xsl:apply-templates select="@* | node()" />
13
+ </xsl:copy>
14
+ </xsl:template>
15
+
16
+ <!-- <xsl:template match="akn:identification/FRBRWork/FRBRthistext/text()"><xsl:copy-of select="$cat" /></xsl:template> -->
17
+
18
+ <xsl:template match="akn:identification/akn:FRBRWork/akn:FRBRthis">
19
+ <akn:FRBRthis>
20
+ <xsl:attribute name="value">
21
+ <xsl:value-of select="$work_uri" />
22
+ </xsl:attribute>
23
+ </akn:FRBRthis>
24
+ </xsl:template>
25
+
26
+ <xsl:template match="akn:identification/akn:FRBRWork/akn:FRBRuri">
27
+ <akn:FRBRuri>
28
+ <xsl:attribute name="value">
29
+ <xsl:value-of select="$work_uri" />
30
+ </xsl:attribute>
31
+ </akn:FRBRuri>
32
+ </xsl:template>
33
+
34
+ <xsl:template match="akn:identification/akn:FRBRExpression/akn:FRBRthis">
35
+ <akn:FRBRthis>
36
+ <xsl:attribute name="value">
37
+ <xsl:value-of select="$expression_uri" />
38
+ </xsl:attribute>
39
+ </akn:FRBRthis>
40
+ </xsl:template>
41
+
42
+ <xsl:template match="akn:identification/akn:FRBRExpression/akn:FRBRuri">
43
+ <akn:FRBRuri>
44
+ <xsl:attribute name="value">
45
+ <xsl:value-of select="$expression_uri" />
46
+ </xsl:attribute>
47
+ </akn:FRBRuri>
48
+ </xsl:template>
49
+
50
+ <xsl:template match="akn:identification/akn:FRBRManifestation/akn:FRBRthis">
51
+ <akn:FRBRthis>
52
+ <xsl:attribute name="value">
53
+ <xsl:value-of select="$manifestation_uri" />
54
+ </xsl:attribute>
55
+ </akn:FRBRthis>
56
+ </xsl:template>
57
+
58
+ <xsl:template match="akn:identification/akn:FRBRManifestation/akn:FRBRuri">
59
+ <akn:FRBRuri>
60
+ <xsl:attribute name="value">
61
+ <xsl:value-of select="$manifestation_uri" />
62
+ </xsl:attribute>
63
+ </akn:FRBRuri>
64
+ </xsl:template>
65
+
66
+
67
+ </xsl:stylesheet>
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0"?>
2
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
3
+ xmlns:uk='https://caselaw.nationalarchives.gov.uk/akn'
4
+ xmlns:akn='http://docs.oasis-open.org/legaldocml/ns/akn/3.0'>
5
+
6
+ <xsl:param name="dog" />
7
+ <xsl:param name="cat" />
8
+ <xsl:output method="xml" indent="yes" />
9
+
10
+ <xsl:template match="@* | node()">
11
+ <xsl:copy>
12
+ <xsl:apply-templates select="@* | node()" />
13
+ </xsl:copy>
14
+ </xsl:template>
15
+
16
+ <xsl:template match="akn:text/text()"><xsl:copy-of select="$cat" /></xsl:template>
17
+
18
+ <xsl:template match="akn:attribute">
19
+ <akn:attribute>
20
+ <xsl:attribute name="attribute">
21
+ <xsl:value-of select="$dog" />
22
+ </xsl:attribute>
23
+ </akn:attribute>
24
+ </xsl:template>
25
+
26
+ </xsl:stylesheet>
@@ -1,16 +1,17 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: ds-caselaw-marklogic-api-client
3
- Version: 37.3.1
3
+ Version: 38.0.0
4
4
  Summary: An API client for interacting with the underlying data in Find Caselaw.
5
- Home-page: https://github.com/nationalarchives/ds-caselaw-custom-api-client
6
5
  Keywords: national archives,caselaw
7
6
  Author: The National Archives
8
7
  Requires-Python: >=3.10.0,<4.0.0
9
8
  Classifier: Programming Language :: Python :: 3
10
9
  Classifier: Programming Language :: Python :: 3.10
11
10
  Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
12
13
  Requires-Dist: boto3 (>=1.26.112,<2.0.0)
13
- Requires-Dist: certifi (>=2025.4.26,<2025.5.0)
14
+ Requires-Dist: certifi (>=2025.6.15,<2025.7.0)
14
15
  Requires-Dist: charset-normalizer (>=3.0.0,<4.0.0)
15
16
  Requires-Dist: django-environ (>=0.12.0)
16
17
  Requires-Dist: ds-caselaw-utils (>=2.0.0,<3.0.0)
@@ -26,6 +27,7 @@ Requires-Dist: requests-toolbelt (>=0.10.1,<1.1.0)
26
27
  Requires-Dist: saxonche (>=12.5.0,<13.0.0)
27
28
  Requires-Dist: sqids (>=0.5.0,<0.6.0)
28
29
  Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
30
+ Project-URL: Homepage, https://github.com/nationalarchives/ds-caselaw-custom-api-client
29
31
  Description-Content-Type: text/markdown
30
32
 
31
33
  # The National Archives: Find Case Law
@@ -4,21 +4,22 @@ caselawclient/client_helpers/__init__.py,sha256=eucyUXwUqI72TPw-C5zLcHlMu4GtFY50
4
4
  caselawclient/client_helpers/search_helpers.py,sha256=R99HyRLeYHgsw2L3DOidEqlKLLvs6Tga5rKTuWQViig,1525
5
5
  caselawclient/content_hash.py,sha256=0cPC4OoABq0SC2wYFX9-24DodNigeOqksDxgxQH_hUA,2221
6
6
  caselawclient/errors.py,sha256=JC16fEGq_MRJX-_KFzfINCV2Cqx8o6OWOt3C16rQd84,3142
7
- caselawclient/factories.py,sha256=XgrnG61hAJ-ty61DLv2qNEAo9ITuDZowZ4MRmPrf2Bg,7109
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=zDrR6XniqB6S2BkNJH8KfvPz-RCBjPVG_N6eJkJiIlA,19467
11
- caselawclient/models/documents/body.py,sha256=IVcuSbgA7SvCvii_VE8MZy_54enDZY8VPoESkUDgfKU,6114
12
- caselawclient/models/documents/exceptions.py,sha256=Mz1P8uNqf5w6uLnRwJt6xK7efsVqtd5VA-WXUUH7QLk,285
10
+ caselawclient/models/documents/__init__.py,sha256=3iEzgjX4ooLUKEuAeOylEUTm5rUQkrRztBmv4Rp1Ub4,21117
11
+ caselawclient/models/documents/body.py,sha256=O1ZTV3KHo-YNi7Syd4oCV1CVSuRF7mcLXojwshyY4jg,6601
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=HlmPb63lLMnySSOLP4iexcAyQiLByKBZtTd25f8sY8M,1268
16
- caselawclient/models/identifiers/__init__.py,sha256=_iAsx_e-Rxq3ED3TQQ5Vg1ZNnQn6AZQK8QCvHSFhdGg,6848
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
17
18
  caselawclient/models/identifiers/exceptions.py,sha256=ckVsjPzLuTXkbd7KZRJXoxcltQCXPGL2rMyYwE5orgg,177
18
- caselawclient/models/identifiers/fclid.py,sha256=Gq0G0eLdUH3j7VybluqJGVcHV3Y-hpjty1OdR91dI5Q,1453
19
- caselawclient/models/identifiers/neutral_citation.py,sha256=mFc-nYDIkLf5fM2eTSBnBigVgxuDcUF2HuMwQYEYeXY,2871
20
- caselawclient/models/identifiers/press_summary_ncn.py,sha256=CKhNTnO6pfriS4Sg-gt0Pf0fRhH53Uf0flULPncJWM4,762
21
- caselawclient/models/identifiers/unpacker.py,sha256=fKfiuTKaN-71e40PV8BC9PueKjwgfX9Xw3IVdy8mkqg,2299
19
+ caselawclient/models/identifiers/fclid.py,sha256=hj8z-VhXFrUHKOY6k_ItPvOakIvbhJ5xEbZ04E2j7t8,1521
20
+ caselawclient/models/identifiers/neutral_citation.py,sha256=bYAeXHVm_ls0aDTeYI4uv35iZmJGSKU4-H-iLh2xED0,2912
21
+ caselawclient/models/identifiers/press_summary_ncn.py,sha256=t-x6PsEe2tz1uO1qZKXKK0TugkQYb_49O_xgjd_oiE4,801
22
+ caselawclient/models/identifiers/unpacker.py,sha256=OpFBw1B6pqSuzcyHbnTY3dScHc2Ujt5StGRnh-tKE1Q,2592
22
23
  caselawclient/models/judgments.py,sha256=r40irgdEID-NeSNLm3OUdUBznMpRSwjD2SJrGlBgP8o,2208
23
24
  caselawclient/models/neutral_citation_mixin.py,sha256=jAac3PPuWyPdj9N-n-U_JfwkbgbSIXaqFVQahfu95do,2086
24
25
  caselawclient/models/parser_logs.py,sha256=30kF4w0GcowiMIFtymUkl7ZARanNh_PjDpJZezn-cA8,315
@@ -30,10 +31,10 @@ caselawclient/models/utilities/move.py,sha256=MXdUqkSiyqRb8YKs_66B6ICWn8EWM6DiJV
30
31
  caselawclient/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
32
  caselawclient/responses/__init__.py,sha256=2-5NJn_PXPTje_W4dHeHYaNRN6vXK4UcB9eLLNUAKa4,67
32
33
  caselawclient/responses/search_response.py,sha256=Z76Zj4VvM-EV_vdiehv2-Jfkr9HZD3SvCTlRrUB_cyE,1951
33
- caselawclient/responses/search_result.py,sha256=sGVIJMLXuN2A2MFPPDoOkXu3SE4wO5IcelLZj4yfWuY,9385
34
+ caselawclient/responses/search_result.py,sha256=gjSpnfygw9mEgKa3_sL-6O8JkGYSyxmrPpwwMMuf-ZQ,9416
34
35
  caselawclient/responses/xsl/search_match.xsl,sha256=4Sv--MrwBd7J48E9aI7jlFSXGlNi4dBqgzJ3bdMJ_ZU,1018
35
36
  caselawclient/search_parameters.py,sha256=A-9icXdyFYLDACjUaRQF8mrnaVRlFJ9XCPtu5uZ-_Lo,3484
36
- caselawclient/types.py,sha256=0PsEsQzJOInpuwZ-Ydw0UpGns6jbHzlJhc-Ms28sqCU,2064
37
+ caselawclient/types.py,sha256=5lE_0kRd7ZMUkr53Rh4ooo74Ab8JTXnMo8rNrwn_HUo,2578
37
38
  caselawclient/xml_helpers.py,sha256=xCboRhhzezqh-VyoKBQwal5lwxv96vTMJyVFWJNN-ok,639
38
39
  caselawclient/xquery/break_judgment_checkout.xqy,sha256=rISzoBKxQKrP5ZRdCSoRqOXW8T_NDBSZRFjOXo_H3ns,220
39
40
  caselawclient/xquery/checkin_judgment.xqy,sha256=QeGqO3kL-q0UrjopCVU0lCbkwbyoc5SuNLYFAIbbyMg,197
@@ -83,7 +84,9 @@ caselawclient/xquery/validate_document.xqy,sha256=PgaDcnqCRJPIVqfmWsNlXmCLNKd21q
83
84
  caselawclient/xquery/xslt.xqy,sha256=w57wNijH3dkwHkpKeAxqjlghVflQwo8cq6jS_sm-erM,199
84
85
  caselawclient/xquery/xslt_transform.xqy,sha256=cccaFiGkCcvSfDv007UriZ3I4ak2nTLP1trRZdbOoS8,2462
85
86
  caselawclient/xquery_type_dicts.py,sha256=zuyDGTkcN6voOXCm3APXItZ-Ey6tZ2hdZummZWzjl50,6489
86
- ds_caselaw_marklogic_api_client-37.3.1.dist-info/LICENSE.md,sha256=fGMzyyLuQW-IAXUeDSCrRdsYW536aEWThdbpCjo6ZKg,1108
87
- ds_caselaw_marklogic_api_client-37.3.1.dist-info/METADATA,sha256=RzqWJC6SHrgrCLLOl4aRL8Th3lhaycwYMR8uATPDZeg,4206
88
- ds_caselaw_marklogic_api_client-37.3.1.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
89
- ds_caselaw_marklogic_api_client-37.3.1.dist-info/RECORD,,
87
+ caselawclient/xslt/modify_xml_live.xsl,sha256=F-0FM0OhT9Ua1WVEVeHYwwVkktmTOERykOuGhMrKans,2346
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.5.2
2
+ Generator: poetry-core 2.1.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any