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    }
DEFAULT_DOCUMENT_BODY_XML = '<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:uk="https://caselaw.nationalarchives.gov.uk/akn">\n <judgment name="decision">\n <meta/><header><p>Header contains text</p></header>\n <judgmentBody>\n <decision>\n <p>This is a document.</p>\n </decision>\n </judgmentBody>\n </judgment>\n </akomaNtoso>'
class DocumentBodyFactory:
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
PARAMS_MAP: dict[str, typing.Any] = {'name': 'Judgment v Judgement', 'court': 'Court of Testing', 'document_date_as_string': '2023-02-03'}
@classmethod
def build( cls, xml_string: str = '<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0" xmlns:uk="https://caselaw.nationalarchives.gov.uk/akn">\n <judgment name="decision">\n <meta/><header><p>Header contains text</p></header>\n <judgmentBody>\n <decision>\n <p>This is a document.</p>\n </decision>\n </judgmentBody>\n </judgment>\n </akomaNtoso>', **kwargs: Any) -> caselawclient.models.documents.body.DocumentBody:
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
class DocumentFactory:
 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
PARAMS_MAP: dict[str, typing.Any] = {'is_published': False, 'is_sensitive': False, 'is_anonymised': False, 'is_failure': False, 'source_name': 'Example Uploader', 'source_email': 'uploader@example.com', 'consignment_reference': 'TDR-12345', 'first_published_datetime': None, 'has_ever_been_published': False, 'assigned_to': '', 'versions': []}
target_class: TypeAlias = caselawclient.models.documents.Document
@classmethod
def build( cls, uri: caselawclient.types.DocumentURIString = 'test/2023/123', api_client: Optional[caselawclient.Client.MarklogicApiClient] = None, identifiers: Optional[list[caselawclient.models.identifiers.Identifier]] = None, **kwargs: Any) -> caselawclient.models.documents.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
class JudgmentFactory(DocumentFactory):
107class JudgmentFactory(DocumentFactory):
108    target_class: TypeAlias = Judgment
109    PARAMS_MAP = DocumentFactory.PARAMS_MAP | {
110        "neutral_citation": "[2023] Test 123",
111    }
target_class: TypeAlias = caselawclient.models.judgments.Judgment
PARAMS_MAP = {'is_published': False, 'is_sensitive': False, 'is_anonymised': False, 'is_failure': False, 'source_name': 'Example Uploader', 'source_email': 'uploader@example.com', 'consignment_reference': 'TDR-12345', 'first_published_datetime': None, 'has_ever_been_published': False, 'assigned_to': '', 'versions': [], 'neutral_citation': '[2023] Test 123'}
Inherited Members
DocumentFactory
build
class PressSummaryFactory(DocumentFactory):
114class PressSummaryFactory(DocumentFactory):
115    target_class: TypeAlias = PressSummary
116    PARAMS_MAP = DocumentFactory.PARAMS_MAP | {
117        "neutral_citation": "[2023] Test 123",
118    }
PARAMS_MAP = {'is_published': False, 'is_sensitive': False, 'is_anonymised': False, 'is_failure': False, 'source_name': 'Example Uploader', 'source_email': 'uploader@example.com', 'consignment_reference': 'TDR-12345', 'first_published_datetime': None, 'has_ever_been_published': False, 'assigned_to': '', 'versions': [], 'neutral_citation': '[2023] Test 123'}
Inherited Members
DocumentFactory
build
class SimpleFactory(typing.Generic[~T]):
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
target_class: Type[~T]
PARAMS_MAP: dict[str, typing.Any]
@classmethod
def build(cls, **kwargs: Any) -> ~T:
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
PARAMS_MAP = {'author': 'Fake Name', 'author_email': 'fake.email@gov.invalid', 'consignment_reference': 'TDR-2023-ABC', 'submission_datetime': datetime.datetime(2023, 2, 3, 9, 12, 34), 'editor_status': 'New'}
Inherited Members
SimpleFactory
build
class IdentifierResolutionFactory:
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))
@classmethod
def build( self, resolution_uuid: Optional[str] = None, document_uri: Optional[str] = None, identifier_slug: Optional[str] = None, published: Optional[bool] = True, namespace: Optional[str] = None, value: Optional[str] = None) -> caselawclient.identifier_resolution.IdentifierResolution:
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))
class IdentifierResolutionsFactory:
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)
@classmethod
def build( self, resolutions: Optional[list[caselawclient.identifier_resolution.IdentifierResolution]] = None) -> caselawclient.identifier_resolution.IdentifierResolutions:
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
PARAMS_MAP = {'uri': 'd-a1b2c3', 'name': 'Judgment v Judgement', 'neutral_citation': '[2025] UKSC 123', 'court': 'Court of Testing', 'date': datetime.datetime(2023, 2, 3, 0, 0), 'transformation_date': '2023-02-03 12:34:00', 'metadata': <Mock name='mock()' id='140209815698800'>, 'is_failure': False, 'matches': None, 'slug': 'uksc/2025/1', 'content_hash': 'ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73', 'identifiers': {'id-1': <Neutral Citation Number [2025] UKSC 123: id-1>, 'id-2': <Find Case Law Identifier bcdfghjk: id-2>}}
Inherited Members
SimpleFactory
build