Skip to content

Commit

Permalink
refactor: initial implementation of generic search
Browse files Browse the repository at this point in the history
implements a version of search in the core package for all endpoints to
use, this performs a number of cleanups:

- removes unused search method signatures and merges them into core
- updates enum with a generic signature that can be reused
- updates test case for testing cardholder profiles
- cleans up core to accept parameters as part of the internal get call

refs #51
  • Loading branch information
devraj committed Jul 5, 2024
1 parent 594a43a commit ef1fc61
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 29 deletions.
10 changes: 0 additions & 10 deletions gallagher/cc/cardholders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ async def get_config(cls) -> EndpointConfig:
dto_retrieve=CardholderDetail,
)

@classmethod
async def search(cls, name: str, sort: str = "id", top: int = 100):
pass


class PdfDefinition(APIEndpoint):
"""PDF Definitions provide a list of support PDF definitions for the instance.
Expand All @@ -53,11 +48,6 @@ async def get_config(cls) -> EndpointConfig:
dto_retrieve=PdfDetail,
)

@classmethod
async def search(cls, name: str, sort: str = "id", top: int = 100):
pass



__shillelagh__ = (
Cardholder,
Expand Down
1 change: 1 addition & 0 deletions gallagher/cc/cardholders/card_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ async def get_config(cls) -> EndpointConfig:
dto_list=CardTypeResponse,
dto_retrieve=CardTypeResponse,
)

73 changes: 57 additions & 16 deletions gallagher/cc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from typing import (
Optional,
Tuple,
Any,
List,
)

from datetime import datetime
Expand All @@ -40,6 +42,10 @@
DiscoveryResponse,
)

from ..enum import (
SearchSortOrder,
)

from ..exception import (
UnlicensedFeatureException,
NotFoundException,
Expand All @@ -60,6 +66,22 @@ def _check_api_key_format(api_key):
return api_tokens.count() == 8


def _sanitise_name_param(name: str) -> str:
"""
Limits the returned items to those with a name that matches this string.
Without surrounding quotes or a percent sign or underscore,
it is a substring match; surround the parameter with double quotes "..."
for an exact match. Without quotes, a percent sign % will match any substring
and an underscore will match any single character.
"""
if name.startswith('"') and name.endswith('"'):
return name

if name.startswith("%") or name.startswith("_"):
return name

return f"%{name}%"

def _get_authorization_headers():
"""Creates an authorization header for Gallagher API calls
Expand Down Expand Up @@ -220,6 +242,7 @@ async def _discover(cls):
from . import api_base

async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
# Don't use the _get wrapper here, we need to get the raw response
response = await _httpx_async.get(
api_base,
headers=_get_authorization_headers(),
Expand Down Expand Up @@ -300,18 +323,29 @@ async def delete(cls):
@classmethod
async def search(
cls,
sort: SearchSortOrder = SearchSortOrder.ID,
top: int = 100,
sort: str = "id",
fields: str = "defaults",
name: Optional[str] = None, # TODO: en
division: str = None, # TODO: use division type
direct_division: str = None, # TODO: use division type
description: Optional[str] = None,
fields: str | List[str] = "defaults",
**kwargs,
):
"""Search wrapper for most objects to dynamically search content
Each object has a set of fields that you can query for, most searches
also allow you to search for a partial string.
We typically use the detail endpoint to run the query once the parameters
have been constructed from the set defined in this method, and any
extras that are passed as part of the kwargs.
direct_division is a division whos ancestors are not included in the search
:param int top: Number of results to return
:param str sort: Sort order, can be set to id or -id
:param str name: Name of the object to search for
:param str division: Division to search for
:param str direct_division: Direct division to search for
:param str description: Description to search for
:param str fields: List of fields to return
:param kwargs: Fields to search for
Expand All @@ -324,23 +358,28 @@ async def search(
"fields": fields,
}

# Adds arbitrary fields to the search, these will be different
# for each type of object that calls the base function
params.update(kwargs)
if name:
params["name"] = name

async with httpx.AsyncClient(proxy=proxy_address) as _httpx_async:
if division:
params["division"] = division

response = await _httpx_async.get(
f"{cls.__config__.endpoint.href}",
params=params,
headers=_get_authorization_headers(),
)
if direct_division:
params["directDivision"] = direct_division

await _httpx_async.aclose()
if description:
params["description"] = description

# Adds arbitrary fields to the search, these will be different
# for each type of object that calls the base function
params.update(kwargs)

parsed_obj = cls.__config__.dto_list.model_validate(response.json())
return await cls._get(
cls.__config__.endpoint.href,
cls.__config__.dto_list,
params=params,
)

return parsed_obj

# Follow links methods, these are valid based on if the response
# classes make available a next, previous or update href, otherwise
Expand Down Expand Up @@ -423,6 +462,7 @@ async def _get(
cls,
url: str,
response_class: AppBaseModel | None,
params: dict[str, Any] = {},
):
"""Generic _get method for all endpoints
Expand All @@ -444,6 +484,7 @@ async def _get(
response = await _httpx_async.get(
f"{url}", # required to turn pydantic object to str
headers=_get_authorization_headers(),
params=params,
)

await _httpx_async.aclose()
Expand Down
8 changes: 6 additions & 2 deletions gallagher/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
from enum import Enum


class CustomerSort(Enum):
"""Sort descriptors for the Customer object"""
class SearchSortOrder(Enum):
"""Sort descriptors for search operations.
If an endpoint needs to customise the sort order,
it should subclass this Enum and add the additional params.
"""

ID: str = "id"
ID_DESC: str = "-id"
Expand Down
19 changes: 18 additions & 1 deletion tests/test_cardholder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""

import random
import pytest

from gallagher.dto.detail import (
Expand Down Expand Up @@ -34,6 +35,22 @@ async def test_cardholder_list(cardholder_summary: CardholderSummaryResponse):
assert len(cardholder_summary.results) > 0


async def test_cardholder_search(cardholder_summary: CardholderSummaryResponse):
"""Test for the cardholder search"""

# Get a random cardholder in the list
cardholder = random.choice(cardholder_summary.results)

# Search for the cardholder
search_results = await Cardholder.search(
name=cardholder.first_name,
)

# We should fine at least one cardholder with the same name
assert type(search_results) is CardholderSummaryResponse
assert len(search_results.results) > 0


async def test_cardholder_detail(cardholder_summary: CardholderSummaryResponse):
"""For each cardholder in the list, get the detail and compare"""

Expand All @@ -55,4 +72,4 @@ async def test_cardholder_detail(cardholder_summary: CardholderSummaryResponse):
# the model_validator would have assigned this via reference
assert getattr(cardholder_detail_response.pdf, pdf_attr_name) \
== pdf.contents


0 comments on commit ef1fc61

Please sign in to comment.