Skip to content

Commit

Permalink
Merge from main
Browse files Browse the repository at this point in the history
  • Loading branch information
elementechemlyn committed Jan 24, 2025
2 parents f0f39e1 + 64b74a7 commit b894db1
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"python.testing.pytestArgs": [
"test"
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
|-|-|-|
| ![Python][python-badge] | [![Bunny Docker Images][docker-badge]][bunny-containers] | [![Bunny Docs][docs-badge]][bunny-docs] |

An HDR UK Cohort Discovery Task Resolver.
A Cohort Discovery Task Resolver.

Fetches and resolves Availability and Distribution Queries against an OMOP-CDM database.
Fetches and resolves Availability and Distribution Queries against an OMOP CDM database.

[hutch-logo]: https://raw.githubusercontent.com/HDRUK/hutch/main/assets/Hutch%20splash%20bg.svg
[hutch-repo]: https://github.com/health-informatics-uon/hutch

[bunny-docs]: https://health-informatics-uon.github.io/hutch/bunny
[bunny-containers]: https://github.com/Health-Informatics-UoN/hutch-cohort-discovery/pkgs/container/hutch%2Fbunny
[bunny-containers]: https://github.com/Health-Informatics-UoN/hutch-bunny/pkgs/container/hutch%2Fbunny

[license-badge]: https://img.shields.io/github/license/health-informatics-uon/hutch-cohort-discovery.svg
[license-badge]: https://img.shields.io/github/license/health-informatics-uon/hutch-bunny.svg
[python-badge]: https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white
[docker-badge]: https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white
[docs-badge]: https://img.shields.io/badge/docs-black?style=for-the-badge&labelColor=%23222
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = ["ruff>=0.8.6", "pytest>=8.3.4"]
dev = [
"ruff>=0.8.6",
"pytest>=8.3.4",
"pandas-stubs>=2.2.3.241126",
"mypy>=1.14.1",
"types-requests>=2.32.0.20241016",
]
2 changes: 1 addition & 1 deletion src/hutch_bunny/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from hutch_bunny.core.obfuscation import get_results_modifiers_from_str
from hutch_bunny.core.results_modifiers import get_results_modifiers_from_str
from hutch_bunny.core.execute_query import execute_query
from hutch_bunny.core.rquest_dto.result import RquestResult
from hutch_bunny.core.parser import parser
Expand Down
4 changes: 2 additions & 2 deletions src/hutch_bunny/core/execute_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from hutch_bunny.core import query_solvers
from hutch_bunny.core.rquest_dto.query import AvailabilityQuery, DistributionQuery
from hutch_bunny.core.obfuscation import (
apply_filters_v2,
apply_filters,
)
from hutch_bunny.core.rquest_dto.result import RquestResult

Expand Down Expand Up @@ -50,7 +50,7 @@ def execute_query(
result = query_solvers.solve_availability(
db_manager=db_manager, query=query
)
result.count = apply_filters_v2(result.count, results_modifiers)
result.count = apply_filters(result.count, results_modifiers)
return result
except TypeError as te: # raised if the distribution query json format is wrong
logger.error(str(te), exc_info=True)
Expand Down
85 changes: 7 additions & 78 deletions src/hutch_bunny/core/obfuscation.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,8 @@
import json
import os
import requests
from typing import Union


def get_results_modifiers(activity_source_id: int) -> list:
"""Get the results modifiers for a given activity source.
Args:
activity_source_id (int): The acivity source ID.
Returns:
list: The modifiers for the given activity source.
Raises:
HTTPError: raised when this function can't get the results modifiers.
"""
res = requests.get(
f"{os.getenv('MANAGER_URL')}/api/activitysources/{activity_source_id}/resultsmodifiers",
verify=int(os.getenv("MANAGER_VERIFY_SSL", 1)),
)
res.raise_for_status()
modifiers = res.json()
return modifiers


def get_results_modifiers_from_str(params: str) -> list:
"""Deserialise a JSON list containing results modifiers
Args:
params (str):
The JSON string containing list of parameter objects for results modifiers
Raises:
ValueError: The parsed string does not produce a list
Returns:
list: The list of parameter dicts of results modifiers
"""
deserialised_params = json.loads(params)
if not isinstance(deserialised_params, list):
raise ValueError(
f"{get_results_modifiers_from_str.__name__} requires a JSON list"
)
return deserialised_params


def low_number_suppression(
value: Union[int, float], threshold: int = 10
) -> Union[int, float]:
def low_number_suppression(value: int | float, threshold: int = 10) -> int | float:
"""Suppress values that fall below a given threshold.
Args:
value (Union[int, float]): The value to evaluate.
value (int | float): The value to evaluate.
threshold (int): The threshold to beat.
Returns:
Expand All @@ -67,11 +17,11 @@ def low_number_suppression(
return value if value > threshold else 0


def rounding(value: Union[int, float], nearest: int = 10) -> int:
def rounding(value: int | float, nearest: int = 10) -> int:
"""Round the value to the nearest base number, e.g. 10.
Args:
value (Union[int, float]): The value to be rounded
value (int | float): The value to be rounded
nearest (int, optional): Round value to this base. Defaults to 10.
Returns:
Expand All @@ -86,36 +36,15 @@ def rounding(value: Union[int, float], nearest: int = 10) -> int:
return nearest * round(value / nearest)


def apply_filters(value: Union[int, float], filters: list) -> Union[int, float]:
"""Iterate over a list of filters from the Manager and apply them to the
supplied value.
Args:
value (Union[int, float]): The value to be filtered.
filters (list): The filters applied to the value.
Returns:
Union[int, float]: The filtered value.
"""
actions = {"Low Number Suppression": low_number_suppression, "Rounding": rounding}
result = value
for f in filters:
if action := actions.get(f["type"]["id"]):
result = action(result, **f["parameters"])
if result == 0:
break # don't apply any more filters
return result


def apply_filters_v2(value: Union[int, float], filters: list) -> Union[int, float]:
def apply_filters(value: int | float, filters: list) -> int | float:
"""Iterate over a list of filters and apply them to the supplied value.
Args:
value (Union[int, float]): The value to be filtered.
value (int | float): The value to be filtered.
filters (list): The filters applied to the value.
Returns:
Union[int, float]: The filtered value.
int | float: The filtered value.
"""
actions = {"Low Number Suppression": low_number_suppression, "Rounding": rounding}
result = value
Expand Down
24 changes: 24 additions & 0 deletions src/hutch_bunny/core/results_modifiers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json


def results_modifiers(
low_number_suppression_threshold: int,
rounding_target: int,
Expand All @@ -18,3 +21,24 @@ def results_modifiers(
}
)
return results_modifiers


def get_results_modifiers_from_str(params: str) -> list:
"""Deserialise a JSON list containing results modifiers
Args:
params (str):
The JSON string containing list of parameter objects for results modifiers
Raises:
ValueError: The parsed string does not produce a list
Returns:
list: The list of parameter dicts of results modifiers
"""
deserialised_params = json.loads(params)
if not isinstance(deserialised_params, list):
raise ValueError(
f"{get_results_modifiers_from_str.__name__} requires a JSON list"
)
return deserialised_params
Empty file added tests/__init__.py
Empty file.
1 change: 1 addition & 0 deletions tests/test_demographics_distribution_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dotenv import load_dotenv
import os
import hutch_bunny.core.settings as settings
import hutch_bunny.core.setting_database as db_settings

load_dotenv()

Expand Down
66 changes: 66 additions & 0 deletions tests/test_obfuscation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from hutch_bunny.core.obfuscation import (
apply_filters,
low_number_suppression,
rounding,
)


def test_low_number_suppression():
# Test that the threshold is applied
assert low_number_suppression(99, threshold=100) == 0
assert low_number_suppression(100, threshold=100) == 0
assert low_number_suppression(101, threshold=100) == 101

# Test that the threshold can be set to 0
assert low_number_suppression(1, threshold=0) == 1

# Test negative threshold is ignored
assert low_number_suppression(1, threshold=-5) == 1


def test_rounding():
# Test default nearest
assert rounding(9) == 10

# Test rounding is applied
assert rounding(123, nearest=100) == 100
assert rounding(123, nearest=10) == 120
assert rounding(123, nearest=1) == 123

# Test rounding is applied the boundary
assert rounding(150, nearest=100) == 200


def test_apply_filters_rounding():
# Test rounding only
filters = [{"id": "Rounding", "nearest": 100}]
assert apply_filters(123, filters=filters) == 100


def test_apply_filters_low_number_suppression():
# Test low number suppression only
filters = [{"id": "Low Number Suppression", "threshold": 100}]
assert apply_filters(123, filters=filters) == 123


def test_apply_filters_combined():
# Test both filters
filters = [
{"id": "Low Number Suppression", "threshold": 100},
{"id": "Rounding", "nearest": 100},
]
assert apply_filters(123, filters=filters) == 100


def test_apply_filters_combined_leak():
# Test that putting the rounding filter first can leak the low number suppression filter
filters = [
{"id": "Rounding", "nearest": 100},
{"id": "Low Number Suppression", "threshold": 70},
]
assert apply_filters(60, filters=filters) == 100


def test_apply_filters_combined_empty_filter():
# Test that an empty filter list returns the original value
assert apply_filters(9, []) == 9
1 change: 1 addition & 0 deletions tests/test_return.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from dotenv import load_dotenv
import os
import hutch_bunny.core.settings as settings
import hutch_bunny.core.setting_database as db_settings

load_dotenv()

Expand Down
Loading

0 comments on commit b894db1

Please sign in to comment.