caselawclient.factories
1import datetime 2import json 3from typing import Any, Generic, Optional, Type, TypeAlias, TypeVar, cast 4from unittest.mock import Mock 5 6from caselawclient.Client import MarklogicApiClient 7from caselawclient.identifier_resolution import IdentifierResolution, IdentifierResolutions 8from caselawclient.models.documents import Document 9from caselawclient.models.documents.body import DocumentBody 10from caselawclient.models.identifiers import Identifier 11from caselawclient.models.identifiers.collection import IdentifiersCollection 12from caselawclient.models.identifiers.fclid import FindCaseLawIdentifier 13from caselawclient.models.identifiers.neutral_citation import NeutralCitationNumber 14from caselawclient.models.judgments import Judgment 15from caselawclient.models.press_summaries import PressSummary 16from caselawclient.responses.search_result import SearchResult, SearchResultMetadata 17from caselawclient.types import DocumentURIString 18 19T = TypeVar("T") 20 21DEFAULT_DOCUMENT_BODY_XML = """<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:uk="https://caselaw.nationalarchives.gov.uk/akn"> 22 <judgment name="decision"> 23 <meta/><header><p>Header contains text</p></header> 24 <judgmentBody> 25 <decision> 26 <p>This is a document.</p> 27 </decision> 28 </judgmentBody> 29 </judgment> 30 </akomaNtoso>""" 31 32 33class DocumentBodyFactory: 34 # "name_of_attribute": "default value" 35 PARAMS_MAP: dict[str, Any] = { 36 "name": "Judgment v Judgement", 37 "court": "Court of Testing", 38 "document_date_as_string": "2023-02-03", 39 } 40 41 @classmethod 42 def build(cls, xml_string: str = DEFAULT_DOCUMENT_BODY_XML, **kwargs: Any) -> DocumentBody: 43 document_body = DocumentBody( 44 xml_bytestring=xml_string.encode(encoding="utf-8"), 45 ) 46 47 for param_name, default_value in cls.PARAMS_MAP.items(): 48 value = kwargs.get(param_name, default_value) 49 setattr(document_body, param_name, value) 50 51 return document_body 52 53 54class DocumentFactory: 55 # "name_of_attribute": "default value" 56 PARAMS_MAP: dict[str, Any] = { 57 "is_published": False, 58 "is_sensitive": False, 59 "is_anonymised": False, 60 "is_failure": False, 61 "source_name": "Example Uploader", 62 "source_email": "uploader@example.com", 63 "consignment_reference": "TDR-12345", 64 "first_published_datetime": None, 65 "has_ever_been_published": False, 66 "assigned_to": "", 67 "versions": [], 68 } 69 70 target_class: TypeAlias = Document 71 72 @classmethod 73 def build( 74 cls, 75 uri: DocumentURIString = DocumentURIString("test/2023/123"), 76 api_client: Optional[MarklogicApiClient] = None, 77 identifiers: Optional[list[Identifier]] = None, 78 **kwargs: Any, 79 ) -> target_class: 80 def _fake_linked_documents(*args: Any, **kwargs: Any) -> list["Document"]: 81 return [document] 82 83 if not api_client: 84 api_client = Mock(spec=MarklogicApiClient) 85 api_client.get_judgment_xml_bytestring.return_value = DEFAULT_DOCUMENT_BODY_XML.encode(encoding="utf-8") 86 api_client.get_property_as_node.return_value = None 87 88 document = cls.target_class(uri, api_client=api_client) 89 document.body = kwargs.pop("body") if "body" in kwargs else DocumentBodyFactory.build() 90 91 if identifiers is None: 92 document.identifiers.add(FindCaseLawIdentifier(value="tn4t35ts")) 93 else: 94 for identifier in identifiers: 95 document.identifiers.add(identifier) 96 97 setattr(document, "linked_documents", _fake_linked_documents) 98 99 for param_name, default_value in cls.PARAMS_MAP.items(): 100 value = kwargs.get(param_name, default_value) 101 setattr(document, param_name, value) 102 103 return document 104 105 106class JudgmentFactory(DocumentFactory): 107 target_class: TypeAlias = Judgment 108 PARAMS_MAP = DocumentFactory.PARAMS_MAP | { 109 "neutral_citation": "[2023] Test 123", 110 } 111 112 113class PressSummaryFactory(DocumentFactory): 114 target_class: TypeAlias = PressSummary 115 PARAMS_MAP = DocumentFactory.PARAMS_MAP | { 116 "neutral_citation": "[2023] Test 123", 117 } 118 119 120class SimpleFactory(Generic[T]): 121 target_class: Type[T] 122 # "name_of_attribute": "default value" 123 PARAMS_MAP: dict[str, Any] 124 125 @classmethod 126 def build(cls, **kwargs: Any) -> T: 127 mock_object = Mock(spec=cls.target_class, autospec=True) 128 129 for param, default in cls.PARAMS_MAP.items(): 130 if param in kwargs: 131 setattr(mock_object.return_value, param, kwargs[param]) 132 else: 133 setattr(mock_object.return_value, param, default) 134 135 return cast(T, mock_object()) 136 137 138class SearchResultMetadataFactory(SimpleFactory[SearchResultMetadata]): 139 target_class = SearchResultMetadata 140 # "name_of_attribute": "default value" 141 PARAMS_MAP = { 142 "author": "Fake Name", 143 "author_email": "fake.email@gov.invalid", 144 "consignment_reference": "TDR-2023-ABC", 145 "submission_datetime": datetime.datetime(2023, 2, 3, 9, 12, 34), 146 "editor_status": "New", 147 } 148 149 150class IdentifierResolutionFactory: 151 @classmethod 152 def build( 153 self, 154 resolution_uuid: Optional[str] = None, 155 document_uri: Optional[str] = None, 156 identifier_slug: Optional[str] = None, 157 published: Optional[bool] = True, 158 namespace: Optional[str] = None, 159 value: Optional[str] = None, 160 ) -> IdentifierResolution: 161 raw_resolution = { 162 "documents.compiled_url_slugs.identifier_uuid": resolution_uuid or "24b9a384-8bcf-4f20-996a-5c318f8dc657", 163 "documents.compiled_url_slugs.document_uri": document_uri or "/ewca/civ/2003/547.xml", 164 "documents.compiled_url_slugs.identifier_slug": identifier_slug or "ewca/civ/2003/54721", 165 "documents.compiled_url_slugs.document_published": published, 166 "documents.compiled_url_slugs.identifier_namespace": namespace or "ukncn", 167 "documents.compiled_url_slugs.identifier_value": value or "[2003] EWCA 54721 (Civ)", 168 } 169 return IdentifierResolution.from_marklogic_output(json.dumps(raw_resolution)) 170 171 172class IdentifierResolutionsFactory: 173 @classmethod 174 def build(self, resolutions: Optional[list[IdentifierResolution]] = None) -> IdentifierResolutions: 175 if resolutions is None: 176 resolutions = [IdentifierResolutionFactory.build()] 177 return IdentifierResolutions(resolutions) 178 179 180class SearchResultFactory(SimpleFactory[SearchResult]): 181 target_class = SearchResult 182 PARAMS_MAP = { 183 "uri": "d-a1b2c3", 184 "name": "Judgment v Judgement", 185 "neutral_citation": "[2025] UKSC 123", 186 "court": "Court of Testing", 187 "date": datetime.datetime(2023, 2, 3), 188 "transformation_date": str(datetime.datetime(2023, 2, 3, 12, 34)), 189 "metadata": SearchResultMetadataFactory.build(), 190 "is_failure": False, 191 "matches": None, 192 "slug": "uksc/2025/1", 193 "content_hash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73", 194 "identifiers": IdentifiersCollection( 195 { 196 "id-1": NeutralCitationNumber("[2025] UKSC 123", "id-1"), 197 "id-2": FindCaseLawIdentifier("bcdfghjk", "id-2"), 198 } 199 ), 200 }
34class DocumentBodyFactory: 35 # "name_of_attribute": "default value" 36 PARAMS_MAP: dict[str, Any] = { 37 "name": "Judgment v Judgement", 38 "court": "Court of Testing", 39 "document_date_as_string": "2023-02-03", 40 } 41 42 @classmethod 43 def build(cls, xml_string: str = DEFAULT_DOCUMENT_BODY_XML, **kwargs: Any) -> DocumentBody: 44 document_body = DocumentBody( 45 xml_bytestring=xml_string.encode(encoding="utf-8"), 46 ) 47 48 for param_name, default_value in cls.PARAMS_MAP.items(): 49 value = kwargs.get(param_name, default_value) 50 setattr(document_body, param_name, value) 51 52 return document_body
42 @classmethod 43 def build(cls, xml_string: str = DEFAULT_DOCUMENT_BODY_XML, **kwargs: Any) -> DocumentBody: 44 document_body = DocumentBody( 45 xml_bytestring=xml_string.encode(encoding="utf-8"), 46 ) 47 48 for param_name, default_value in cls.PARAMS_MAP.items(): 49 value = kwargs.get(param_name, default_value) 50 setattr(document_body, param_name, value) 51 52 return document_body
55class DocumentFactory: 56 # "name_of_attribute": "default value" 57 PARAMS_MAP: dict[str, Any] = { 58 "is_published": False, 59 "is_sensitive": False, 60 "is_anonymised": False, 61 "is_failure": False, 62 "source_name": "Example Uploader", 63 "source_email": "uploader@example.com", 64 "consignment_reference": "TDR-12345", 65 "first_published_datetime": None, 66 "has_ever_been_published": False, 67 "assigned_to": "", 68 "versions": [], 69 } 70 71 target_class: TypeAlias = Document 72 73 @classmethod 74 def build( 75 cls, 76 uri: DocumentURIString = DocumentURIString("test/2023/123"), 77 api_client: Optional[MarklogicApiClient] = None, 78 identifiers: Optional[list[Identifier]] = None, 79 **kwargs: Any, 80 ) -> target_class: 81 def _fake_linked_documents(*args: Any, **kwargs: Any) -> list["Document"]: 82 return [document] 83 84 if not api_client: 85 api_client = Mock(spec=MarklogicApiClient) 86 api_client.get_judgment_xml_bytestring.return_value = DEFAULT_DOCUMENT_BODY_XML.encode(encoding="utf-8") 87 api_client.get_property_as_node.return_value = None 88 89 document = cls.target_class(uri, api_client=api_client) 90 document.body = kwargs.pop("body") if "body" in kwargs else DocumentBodyFactory.build() 91 92 if identifiers is None: 93 document.identifiers.add(FindCaseLawIdentifier(value="tn4t35ts")) 94 else: 95 for identifier in identifiers: 96 document.identifiers.add(identifier) 97 98 setattr(document, "linked_documents", _fake_linked_documents) 99 100 for param_name, default_value in cls.PARAMS_MAP.items(): 101 value = kwargs.get(param_name, default_value) 102 setattr(document, param_name, value) 103 104 return document
73 @classmethod 74 def build( 75 cls, 76 uri: DocumentURIString = DocumentURIString("test/2023/123"), 77 api_client: Optional[MarklogicApiClient] = None, 78 identifiers: Optional[list[Identifier]] = None, 79 **kwargs: Any, 80 ) -> target_class: 81 def _fake_linked_documents(*args: Any, **kwargs: Any) -> list["Document"]: 82 return [document] 83 84 if not api_client: 85 api_client = Mock(spec=MarklogicApiClient) 86 api_client.get_judgment_xml_bytestring.return_value = DEFAULT_DOCUMENT_BODY_XML.encode(encoding="utf-8") 87 api_client.get_property_as_node.return_value = None 88 89 document = cls.target_class(uri, api_client=api_client) 90 document.body = kwargs.pop("body") if "body" in kwargs else DocumentBodyFactory.build() 91 92 if identifiers is None: 93 document.identifiers.add(FindCaseLawIdentifier(value="tn4t35ts")) 94 else: 95 for identifier in identifiers: 96 document.identifiers.add(identifier) 97 98 setattr(document, "linked_documents", _fake_linked_documents) 99 100 for param_name, default_value in cls.PARAMS_MAP.items(): 101 value = kwargs.get(param_name, default_value) 102 setattr(document, param_name, value) 103 104 return document
107class JudgmentFactory(DocumentFactory): 108 target_class: TypeAlias = Judgment 109 PARAMS_MAP = DocumentFactory.PARAMS_MAP | { 110 "neutral_citation": "[2023] Test 123", 111 }
Inherited Members
114class PressSummaryFactory(DocumentFactory): 115 target_class: TypeAlias = PressSummary 116 PARAMS_MAP = DocumentFactory.PARAMS_MAP | { 117 "neutral_citation": "[2023] Test 123", 118 }
Inherited Members
121class SimpleFactory(Generic[T]): 122 target_class: Type[T] 123 # "name_of_attribute": "default value" 124 PARAMS_MAP: dict[str, Any] 125 126 @classmethod 127 def build(cls, **kwargs: Any) -> T: 128 mock_object = Mock(spec=cls.target_class, autospec=True) 129 130 for param, default in cls.PARAMS_MAP.items(): 131 if param in kwargs: 132 setattr(mock_object.return_value, param, kwargs[param]) 133 else: 134 setattr(mock_object.return_value, param, default) 135 136 return cast(T, mock_object())
Abstract base class for generic types.
On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::
class Mapping[KT, VT]:
def __getitem__(self, key: KT) -> VT:
...
# Etc.
On older versions of Python, however, generic classes have to explicitly inherit from Generic.
After a class has been declared to be generic, it can then be used as follows::
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
try:
return mapping[key]
except KeyError:
return default
126 @classmethod 127 def build(cls, **kwargs: Any) -> T: 128 mock_object = Mock(spec=cls.target_class, autospec=True) 129 130 for param, default in cls.PARAMS_MAP.items(): 131 if param in kwargs: 132 setattr(mock_object.return_value, param, kwargs[param]) 133 else: 134 setattr(mock_object.return_value, param, default) 135 136 return cast(T, mock_object())
139class SearchResultMetadataFactory(SimpleFactory[SearchResultMetadata]): 140 target_class = SearchResultMetadata 141 # "name_of_attribute": "default value" 142 PARAMS_MAP = { 143 "author": "Fake Name", 144 "author_email": "fake.email@gov.invalid", 145 "consignment_reference": "TDR-2023-ABC", 146 "submission_datetime": datetime.datetime(2023, 2, 3, 9, 12, 34), 147 "editor_status": "New", 148 }
Abstract base class for generic types.
On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::
class Mapping[KT, VT]:
def __getitem__(self, key: KT) -> VT:
...
# Etc.
On older versions of Python, however, generic classes have to explicitly inherit from Generic.
After a class has been declared to be generic, it can then be used as follows::
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
try:
return mapping[key]
except KeyError:
return default
Inherited Members
151class IdentifierResolutionFactory: 152 @classmethod 153 def build( 154 self, 155 resolution_uuid: Optional[str] = None, 156 document_uri: Optional[str] = None, 157 identifier_slug: Optional[str] = None, 158 published: Optional[bool] = True, 159 namespace: Optional[str] = None, 160 value: Optional[str] = None, 161 ) -> IdentifierResolution: 162 raw_resolution = { 163 "documents.compiled_url_slugs.identifier_uuid": resolution_uuid or "24b9a384-8bcf-4f20-996a-5c318f8dc657", 164 "documents.compiled_url_slugs.document_uri": document_uri or "/ewca/civ/2003/547.xml", 165 "documents.compiled_url_slugs.identifier_slug": identifier_slug or "ewca/civ/2003/54721", 166 "documents.compiled_url_slugs.document_published": published, 167 "documents.compiled_url_slugs.identifier_namespace": namespace or "ukncn", 168 "documents.compiled_url_slugs.identifier_value": value or "[2003] EWCA 54721 (Civ)", 169 } 170 return IdentifierResolution.from_marklogic_output(json.dumps(raw_resolution))
152 @classmethod 153 def build( 154 self, 155 resolution_uuid: Optional[str] = None, 156 document_uri: Optional[str] = None, 157 identifier_slug: Optional[str] = None, 158 published: Optional[bool] = True, 159 namespace: Optional[str] = None, 160 value: Optional[str] = None, 161 ) -> IdentifierResolution: 162 raw_resolution = { 163 "documents.compiled_url_slugs.identifier_uuid": resolution_uuid or "24b9a384-8bcf-4f20-996a-5c318f8dc657", 164 "documents.compiled_url_slugs.document_uri": document_uri or "/ewca/civ/2003/547.xml", 165 "documents.compiled_url_slugs.identifier_slug": identifier_slug or "ewca/civ/2003/54721", 166 "documents.compiled_url_slugs.document_published": published, 167 "documents.compiled_url_slugs.identifier_namespace": namespace or "ukncn", 168 "documents.compiled_url_slugs.identifier_value": value or "[2003] EWCA 54721 (Civ)", 169 } 170 return IdentifierResolution.from_marklogic_output(json.dumps(raw_resolution))
173class IdentifierResolutionsFactory: 174 @classmethod 175 def build(self, resolutions: Optional[list[IdentifierResolution]] = None) -> IdentifierResolutions: 176 if resolutions is None: 177 resolutions = [IdentifierResolutionFactory.build()] 178 return IdentifierResolutions(resolutions)
181class SearchResultFactory(SimpleFactory[SearchResult]): 182 target_class = SearchResult 183 PARAMS_MAP = { 184 "uri": "d-a1b2c3", 185 "name": "Judgment v Judgement", 186 "neutral_citation": "[2025] UKSC 123", 187 "court": "Court of Testing", 188 "date": datetime.datetime(2023, 2, 3), 189 "transformation_date": str(datetime.datetime(2023, 2, 3, 12, 34)), 190 "metadata": SearchResultMetadataFactory.build(), 191 "is_failure": False, 192 "matches": None, 193 "slug": "uksc/2025/1", 194 "content_hash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73", 195 "identifiers": IdentifiersCollection( 196 { 197 "id-1": NeutralCitationNumber("[2025] UKSC 123", "id-1"), 198 "id-2": FindCaseLawIdentifier("bcdfghjk", "id-2"), 199 } 200 ), 201 }
Abstract base class for generic types.
On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::
class Mapping[KT, VT]:
def __getitem__(self, key: KT) -> VT:
...
# Etc.
On older versions of Python, however, generic classes have to explicitly inherit from Generic.
After a class has been declared to be generic, it can then be used as follows::
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
try:
return mapping[key]
except KeyError:
return default