Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor search to get rid of dependency on static api_client instance #468

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/caselawclient/client_helpers/search_helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from lxml import etree

from caselawclient.Client import MarklogicApiClient
from caselawclient.responses.search_response import SearchResponse
from caselawclient.search_parameters import SearchParameters
Expand All @@ -14,8 +16,11 @@ def search_judgments_and_parse_response(

:return: The parsed search response as a SearchResponse object
"""
return SearchResponse.from_response_string(
api_client.search_judgments_and_decode_response(search_parameters)
return SearchResponse(
jacksonj04 marked this conversation as resolved.
Show resolved Hide resolved
etree.fromstring(
api_client.search_judgments_and_decode_response(search_parameters)
),
api_client,
)


Expand All @@ -30,6 +35,7 @@ def search_and_parse_response(

:return: The parsed search response as a SearchResponse object
"""
return SearchResponse.from_response_string(
api_client.search_and_decode_response(search_parameters)
return SearchResponse(
etree.fromstring(api_client.search_and_decode_response(search_parameters)),
api_client,
)
20 changes: 4 additions & 16 deletions src/caselawclient/responses/search_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from lxml import etree

from caselawclient.Client import MarklogicApiClient
from caselawclient.responses.search_result import SearchResult


Expand All @@ -13,22 +14,14 @@ class SearchResponse:
NAMESPACES = {"search": "http://marklogic.com/appservices/search"}
""" Namespaces used in XPath expressions."""

def __init__(self, node: etree._Element) -> None:
def __init__(self, node: etree._Element, client: MarklogicApiClient) -> None:
"""
Initializes a SearchResponse instance from an xml node.

:param node: The XML data as an etree element
"""
self.node = node

@staticmethod
def from_response_string(xml: str) -> "SearchResponse":
"""
Constructs a SearchResponse instance from an xml response string.

:param xml: The XML data as a string
"""
return SearchResponse(etree.fromstring(xml))
self.client = client

@property
def total(self) -> str:
Expand All @@ -51,9 +44,4 @@ def results(self) -> List[SearchResult]:
results = self.node.xpath(
"//search:response/search:result", namespaces=self.NAMESPACES
)
return [
SearchResult(
result,
)
for result in results
]
return [SearchResult(result, self.client) for result in results]
32 changes: 10 additions & 22 deletions src/caselawclient/responses/search_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import os
from datetime import datetime
from enum import Enum
from functools import cached_property
from typing import Dict, Optional

from dateutil import parser as dateparser
from dateutil.parser import ParserError
from ds_caselaw_utils.courts import Court, CourtNotFoundException, courts
from lxml import etree

from caselawclient.Client import api_client
from caselawclient.Client import MarklogicApiClient
from caselawclient.models.documents import DocumentURIString
from caselawclient.xml_helpers import get_xpath_match_string

Expand Down Expand Up @@ -44,20 +45,6 @@ def __init__(self, node: etree._Element, last_modified: str):
self.node = node
self.last_modified = last_modified

@staticmethod
def create_from_uri(uri: DocumentURIString) -> "SearchResultMetadata":
"""
Create a SearchResultMetadata instance from a search result URI.

:param uri: The URI of the search result

:return: The created SearchResultMetadata instance
"""
response_text = api_client.get_properties_for_search_results([uri])
last_modified = api_client.get_last_modified(uri)
root = etree.fromstring(response_text)
return SearchResultMetadata(root, last_modified)

@property
def author(self) -> str:
"""
Expand Down Expand Up @@ -162,12 +149,13 @@ class SearchResult:
}
""" Namespace mappings used in XPath expressions. """

def __init__(self, node: etree._Element):
def __init__(self, node: etree._Element, client: MarklogicApiClient):
"""
:param node: The XML element representing the search result
"""

self.node = node
self.client = client

@property
def uri(self) -> DocumentURIString:
Expand Down Expand Up @@ -259,15 +247,15 @@ def matches(self) -> str:
xslt_transform = etree.XSLT(etree.parse(file_path))
return str(xslt_transform(self.node))

@property
@cached_property
def metadata(self) -> SearchResultMetadata:
"""
:return: The metadata of the search result
:return: A `SearchResultMetadata` instance representing the metadata of this result
"""

return SearchResultMetadata.create_from_uri(
self.uri,
)
response_text = self.client.get_properties_for_search_results([self.uri])
last_modified = self.client.get_last_modified(self.uri)
root = etree.fromstring(response_text)
return SearchResultMetadata(root, last_modified)

def _get_xpath_match_string(self, path: str) -> str:
return get_xpath_match_string(self.node, path, namespaces=self.NAMESPACES)
116 changes: 63 additions & 53 deletions tests/client/test_search_and_decode_response.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,71 @@
from unittest.mock import patch

from caselawclient.Client import api_client
from caselawclient.Client import MarklogicApiClient
from caselawclient.search_parameters import SearchParameters


@patch("caselawclient.Client.api_client.advanced_search")
def test_search_judgments_and_decode_response(
mock_advanced_search,
valid_search_response_xml,
generate_mock_search_response,
):
"""
Given the search parameters for search_judgments_and_decode_response are valid
And a mocked api_client response with search results
When search_judgments_and_decode_response function is called with the mocked API client
and input parameters
Then the API client's advanced_search method should be called once with the
appropriate parameters
And the search_judgments_and_decode_response function should return the response
xml string with all the search results
"""
mock_advanced_search.return_value = generate_mock_search_response(
valid_search_response_xml
)

search_response = api_client.search_judgments_and_decode_response(
SearchParameters(
query="test query",
court="test court",
judge="test judge",
party="test party",
neutral_citation="test citation",
specific_keyword="test keyword",
date_from="2022-01-01",
date_to="2022-01-31",
page=1,
page_size=10,
class TestSearchAndDecodeResponse:
def setup_method(self):
self.client = MarklogicApiClient(
host="",
username="",
password="",
use_https=False,
user_agent="marklogic-api-client-test",
)
)

mock_advanced_search.assert_called_once_with(
SearchParameters(
query="test query",
court="test court",
judge="test judge",
party="test party",
neutral_citation="test citation",
specific_keyword="test keyword",
order=None,
date_from="2022-01-01",
date_to="2022-01-31",
page=1,
page_size=10,
show_unpublished=False,
only_unpublished=False,
collections=["judgment"],
)
)
def test_search_judgments_and_decode_response(
self,
valid_search_response_xml,
generate_mock_search_response,
):
"""
Given the search parameters for search_judgments_and_decode_response are valid
And a mocked MarklogicApiClient.advanced_search response with search results
When search_judgments_and_decode_response function is called with the mocked API client
and input parameters
Then the API client's advanced_search method should be called once with the
appropriate parameters
And the search_judgments_and_decode_response function should return the response
xml string with all the search results
"""
with patch.object(self.client, "advanced_search") as mock_advanced_search:
mock_advanced_search.return_value = generate_mock_search_response(
valid_search_response_xml
)

search_response = self.client.search_judgments_and_decode_response(
SearchParameters(
query="test query",
court="test court",
judge="test judge",
party="test party",
neutral_citation="test citation",
specific_keyword="test keyword",
date_from="2022-01-01",
date_to="2022-01-31",
page=1,
page_size=10,
)
)

mock_advanced_search.assert_called_once_with(
SearchParameters(
query="test query",
court="test court",
judge="test judge",
party="test party",
neutral_citation="test citation",
specific_keyword="test keyword",
order=None,
date_from="2022-01-01",
date_to="2022-01-31",
page=1,
page_size=10,
show_unpublished=False,
only_unpublished=False,
collections=["judgment"],
)
)

assert search_response == valid_search_response_xml
assert search_response == valid_search_response_xml
28 changes: 21 additions & 7 deletions tests/responses/test_search_response.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import pytest
from lxml import etree

from caselawclient.Client import MarklogicApiClient
from caselawclient.responses.search_response import SearchResponse


class TestSearchResponse:
def setup_method(self):
self.client = MarklogicApiClient(
host="",
username="",
password="",
use_https=False,
user_agent="marklogic-api-client-test",
)

def test_total(
self,
):
Expand All @@ -13,10 +23,13 @@ def test_total(
When calling 'total' on it
Then it should return a string representing the total number of results
"""
search_response = SearchResponse.from_response_string(
'<search:response xmlns:search="http://marklogic.com/appservices/search" total="5">' # noqa: E501
"foo"
"</search:response>"
search_response = SearchResponse(
etree.fromstring(
'<search:response xmlns:search="http://marklogic.com/appservices/search" total="5">' # noqa: E501
"foo"
"</search:response>"
),
self.client,
)

assert search_response.total == "5"
Expand All @@ -32,8 +45,9 @@ def test_results(
Then it should return a list of n SearchResult elements
And each element's node attribute should be as expected
"""
search_response = SearchResponse.from_response_string(
generate_search_response_xml(2 * valid_search_result_xml)
search_response = SearchResponse(
etree.fromstring(generate_search_response_xml(2 * valid_search_result_xml)),
self.client,
)

results = search_response.results
Expand Down Expand Up @@ -64,4 +78,4 @@ def test_when_search_namespace_prefix_not_defined_on_response_xml_syntax_error_r
etree.XMLSyntaxError,
match="Namespace prefix search on response is not defined",
):
SearchResponse.from_response_string(xml_without_namespace)
SearchResponse(etree.fromstring(xml_without_namespace), self.client)
Loading