From 6c9a117b4b10b15e937915888079fa09aa7b8e35 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Wed, 8 Jul 2026 06:22:19 +0200 Subject: [PATCH] feat(pyvolca): classification_match on search_activities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit search_activities only sent classification + classification-value, never the match mode, so a caller could not require exact equality — only the default substring. The engine's /activities route and the MCP tool already accept a classification-mode param; the typed client now exposes it as classification_match (MatchMode, default CONTAINS), mirroring how get_supply_chain/get_consumers serialise ClassificationFilter.mode. Ships pyvolca 0.7.2. README API reference regenerated. --- pyvolca/CHANGELOG.md | 9 +++++++++ pyvolca/README.md | 8 ++++++-- pyvolca/pyproject.toml | 2 +- pyvolca/src/volca/client.py | 11 +++++++++++ pyvolca/tests/conftest.py | 1 + pyvolca/tests/test_dispatch.py | 23 +++++++++++++++++++++++ 6 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pyvolca/CHANGELOG.md b/pyvolca/CHANGELOG.md index 521d7e57..56eaa964 100644 --- a/pyvolca/CHANGELOG.md +++ b/pyvolca/CHANGELOG.md @@ -18,6 +18,15 @@ git cliff --unreleased --tag pyvolca-v0.X.Y # render as a released section Then paste the rendered block at the top of this file and tighten wording. +## [0.7.2] - 2026-07-08 + +### Added + +- `Client.search_activities` gains a `classification_match` argument + (`MatchMode.CONTAINS` default, or `MatchMode.EXACT`). A classification filter + can now require exact equality instead of substring — the match mode the + engine and MCP already accept, finally reachable from the typed client. + ## [0.7.1] - 2026-07-07 ### Fixed diff --git a/pyvolca/README.md b/pyvolca/README.md index ae50dcbb..8f060545 100644 --- a/pyvolca/README.md +++ b/pyvolca/README.md @@ -26,7 +26,7 @@ pyvolca speaks one revision of the engine's JSON wire format; the engine adverti _Generated from `volca._compat` — run `python scripts/gen_api_md.py` to regenerate._ -This build of **pyvolca 0.7.1** speaks wire format **1** and requires a VoLCA engine **≥ v0.8.0**. +This build of **pyvolca 0.7.2** speaks wire format **1** and requires a VoLCA engine **≥ v0.8.0**. @@ -672,7 +672,7 @@ Remove ``dep_name`` from the target database's dependencies. Returns the updated ``DatabaseSetupInfo`` dict. -##### `Client.search_activities(name: str | None = None, *, geo: str | None = None, product: str | None = None, preset: str | None = None, classification: str | None = None, classification_value: str | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, offset: int | None = None, exact: bool = False) -> SearchResults[Activity]` +##### `Client.search_activities(name: str | None = None, *, geo: str | None = None, product: str | None = None, preset: str | None = None, classification: str | None = None, classification_value: str | None = None, classification_match: MatchModeLike | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, offset: int | None = None, exact: bool = False) -> SearchResults[Activity]` Search activities in the current database. @@ -691,6 +691,10 @@ Args: preset: Apply a named classification preset configured in the engine. classification: System name (``"ISIC rev.4 ecoinvent"``). classification_value: Substring within that system's value. + classification_match: How ``classification_value`` is compared — + :class:`MatchMode.CONTAINS` (default, substring) or + :class:`MatchMode.EXACT` (case-insensitive equality). Ignored + when ``classification`` is unset. page: 1-based page number. Must be paired with ``page_size`` — offset cannot be derived from page alone. page_size: Items per page (becomes the wire-level ``limit``). diff --git a/pyvolca/pyproject.toml b/pyvolca/pyproject.toml index 0f1c0a05..7d5e7607 100644 --- a/pyvolca/pyproject.toml +++ b/pyvolca/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyvolca" -version = "0.7.1" +version = "0.7.2" description = "Python client for VoLCA — Life Cycle Assessment engine" requires-python = ">=3.10" license = "Apache-2.0" diff --git a/pyvolca/src/volca/client.py b/pyvolca/src/volca/client.py index b45eed14..7e0af6f1 100644 --- a/pyvolca/src/volca/client.py +++ b/pyvolca/src/volca/client.py @@ -67,6 +67,7 @@ LCIABatchResult, LCIAResult, MatchMode, + MatchModeLike, Method, PathResult, Preset, @@ -849,6 +850,7 @@ def search_activities( preset: str | None = None, classification: str | None = None, classification_value: str | None = None, + classification_match: MatchModeLike | None = None, page: int | None = None, page_size: int | None = None, limit: int | None = None, @@ -872,6 +874,10 @@ def search_activities( preset: Apply a named classification preset configured in the engine. classification: System name (``"ISIC rev.4 ecoinvent"``). classification_value: Substring within that system's value. + classification_match: How ``classification_value`` is compared — + :class:`MatchMode.CONTAINS` (default, substring) or + :class:`MatchMode.EXACT` (case-insensitive equality). Ignored + when ``classification`` is unset. page: 1-based page number. Must be paired with ``page_size`` — offset cannot be derived from page alone. page_size: Items per page (becomes the wire-level ``limit``). @@ -891,6 +897,11 @@ def search_activities( preset=preset, classification=classification, classification_value=classification_value, + classification_mode=( + MatchMode(classification_match).value + if classification_match is not None + else None + ), exact=exact, ) diff --git a/pyvolca/tests/conftest.py b/pyvolca/tests/conftest.py index 705bee7c..004947cd 100644 --- a/pyvolca/tests/conftest.py +++ b/pyvolca/tests/conftest.py @@ -57,6 +57,7 @@ def fixture_spec() -> dict[str, Any]: {"name": "preset", "in": "query", "required": False, "schema": {"type": "string"}}, {"name": "classification", "in": "query", "required": False, "schema": {"type": "string"}}, {"name": "classification-value", "in": "query", "required": False, "schema": {"type": "string"}}, + {"name": "classification-mode", "in": "query", "required": False, "schema": {"type": "string"}}, {"name": "limit", "in": "query", "required": False, "schema": {"type": "integer"}}, {"name": "offset", "in": "query", "required": False, "schema": {"type": "integer"}}, ], diff --git a/pyvolca/tests/test_dispatch.py b/pyvolca/tests/test_dispatch.py index 6a63ee38..547e4588 100644 --- a/pyvolca/tests/test_dispatch.py +++ b/pyvolca/tests/test_dispatch.py @@ -145,6 +145,29 @@ def test_search_activities_drops_none_kwargs(self, mocked_client, make_response, assert "geo" not in params assert "limit" not in params + def test_search_activities_classification_match_sends_mode(self, mocked_client, make_response, empty_envelope): + """classification_match reaches the wire as ``classification-mode``.""" + client, session = mocked_client + session.get.return_value = make_response(empty_envelope()) + client.search_activities(classification="Category", classification_value="Food", classification_match="exact") + params = dict(session.get.call_args[1]["params"]) + assert params["classification-value"] == "Food" + assert params["classification-mode"] == "exact" + + def test_search_activities_omits_mode_by_default(self, mocked_client, make_response, empty_envelope): + """No classification_match → no ``classification-mode`` query param (server default: contains).""" + client, session = mocked_client + session.get.return_value = make_response(empty_envelope()) + client.search_activities(classification="Category", classification_value="Food") + params = dict(session.get.call_args[1]["params"]) + assert "classification-mode" not in params + + def test_search_activities_rejects_bad_match_mode(self, mocked_client): + """A typo'd match mode fails client-side before any HTTP call.""" + client, _ = mocked_client + with pytest.raises(ValueError): + client.search_activities(classification="Category", classification_value="Food", classification_match="exct") + def test_search_activities_page_kwargs_compute_offset(self, mocked_client, make_response, empty_envelope): """page=3 + page_size=20 must translate to offset=40, limit=20.""" client, session = mocked_client