From c4efc49d6e84e3a51506a4c87183836585a00089 Mon Sep 17 00:00:00 2001 From: TheByronHimes Date: Thu, 16 Jul 2026 17:30:54 +0000 Subject: [PATCH 1/3] Fix InMemDao 'or' bug and add test to catch it --- src/hexkit/providers/testing/dao.py | 34 ++++++++++++++++++- .../test_in_mem_dao_against_actual_mongo.py | 14 ++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/hexkit/providers/testing/dao.py b/src/hexkit/providers/testing/dao.py index e122a647..a07c94de 100644 --- a/src/hexkit/providers/testing/dao.py +++ b/src/hexkit/providers/testing/dao.py @@ -173,6 +173,31 @@ def evaluate(self, resource: dict[str, Any]) -> bool: return self._fn(value, self._target_value) +class ConjunctionPredicate(Predicate): + """A Predicate that is satisfied only if all of its sub-predicates are satisfied. + + Represents a single branch of a logical operator that contains multiple + conditions, which MongoDB treats as an implicit $and. + """ + + def __init__(self, *, conditions: list[Predicate]): + """Initialize the predicate with the list of sub-predicates to AND together.""" + self._conditions = conditions + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(conditions={self._conditions})" + + def __eq__(self, other) -> bool: + """Two ConjunctionPredicates are equivalent if all conditions are equivalent.""" + return ( + isinstance(other, ConjunctionPredicate) + and self._conditions == other._conditions + ) + + def evaluate(self, resource: dict[str, Any]) -> bool: + return all(condition.evaluate(resource) for condition in self._conditions) + + class LogicalPredicate(Predicate): """A Predicate that handles MQL logical operators""" @@ -215,7 +240,14 @@ def __init__( if not isinstance(mapping, list) or len(mapping) == 0: raise MQLError(f"The {op} operator must be used with a non-empty list.") for condition in mapping: - self._conditions.extend(build_predicates(mapping=condition)) + branch_predicates = build_predicates(mapping=condition) + if len(branch_predicates) == 1: + self._conditions.extend(branch_predicates) + else: + # A branch with multiple conditions is an implicit $and + self._conditions.append( + ConjunctionPredicate(conditions=branch_predicates) + ) def __repr__(self) -> str: return ( diff --git a/tests/integration/providers/dao/test_in_mem_dao_against_actual_mongo.py b/tests/integration/providers/dao/test_in_mem_dao_against_actual_mongo.py index 838a90b1..79855dda 100644 --- a/tests/integration/providers/dao/test_in_mem_dao_against_actual_mongo.py +++ b/tests/integration/providers/dao/test_in_mem_dao_against_actual_mongo.py @@ -294,6 +294,20 @@ async def test_with_category_dao( ["apples"], id="DateEqualityWithNe", ), + pytest.param( + { + "count": {"$gte": 40}, + "$or": [ + {"other_data.sold_last_week": {"$in": [12, 25]}}, + { + "other_data.sold_last_week": 55, + "other_data.next_restock": {"$ne": None}, + }, + ], + }, + ["apples", "celery", "chain"], + id="FieldCombinedWithOrOfInAndNe", + ), ], ) async def test_with_item_dao( From d87f0780f07364acf4127b500df339e5789117d2 Mon Sep 17 00:00:00 2001 From: TheByronHimes Date: Thu, 16 Jul 2026 17:31:05 +0000 Subject: [PATCH 2/3] Bump version to 8.5.1 --- .pyproject_generation/pyproject_custom.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pyproject_generation/pyproject_custom.toml b/.pyproject_generation/pyproject_custom.toml index c8782702..ddac3b23 100644 --- a/.pyproject_generation/pyproject_custom.toml +++ b/.pyproject_generation/pyproject_custom.toml @@ -1,6 +1,6 @@ [project] name = "hexkit" -version = "8.5.0" +version = "8.5.1" description = "A Toolkit for Building Microservices using the Hexagonal Architecture" requires-python = ">=3.10" classifiers = [ diff --git a/pyproject.toml b/pyproject.toml index a7cdc2eb..c163ac59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Intended Audience :: Developers", ] name = "hexkit" -version = "8.5.0" +version = "8.5.1" description = "A Toolkit for Building Microservices using the Hexagonal Architecture" dependencies = [ "opentelemetry-api >=1.39, <2", From a5e4b8b253144c16290c729583c438ac156c388a Mon Sep 17 00:00:00 2001 From: TheByronHimes Date: Tue, 4 Aug 2026 08:38:29 +0000 Subject: [PATCH 3/3] Update template files --- scripts/list_outdated_dependencies.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/list_outdated_dependencies.py b/scripts/list_outdated_dependencies.py index 10122f71..246b411b 100755 --- a/scripts/list_outdated_dependencies.py +++ b/scripts/list_outdated_dependencies.py @@ -22,7 +22,7 @@ from pathlib import Path from typing import Any, NamedTuple -import httpx +import httpx2 from packaging.requirements import Requirement from script_utils import cli, deps, lock_deps @@ -86,13 +86,13 @@ def get_deps_dev() -> list[Requirement]: return [Requirement(dependency) for dependency in dependencies] -def get_version_from_pypi(package_name: str, client: httpx.Client) -> str: +def get_version_from_pypi(package_name: str, client: httpx2.Client) -> str: """Make a call to PyPI to get the version information about `package_name`.""" try: response = client.get(f"https://pypi.org/pypi/{package_name}/json") body = response.json() version = body["info"]["version"] - except (httpx.RequestError, KeyError): + except (httpx2.RequestError, KeyError): cli.echo_failure(f"Unable to retrieve information for package '{package_name}'") sys.exit(1) @@ -104,7 +104,7 @@ def get_outdated_deps( ) -> list[OutdatedDep]: """Determine which packages have updates available outside of pinned ranges.""" outdated: list[OutdatedDep] = [] - with httpx.Client(timeout=10) as client: + with httpx2.Client(timeout=10) as client: for requirement in requirements: pypi_version = get_version_from_pypi(requirement.name, client)