From d2c20b6c5cb85fa0314ed9f0f5645d89f2da8c5a Mon Sep 17 00:00:00 2001 From: david Date: Thu, 25 Jun 2026 16:50:57 +0200 Subject: [PATCH 1/2] Complete KE client to cover all OpenAPI spec operations Implements three missing operations from the Knowledge Engine OpenAPI spec across the ClientProtocol, Client (HTTP), and TestClient (in-memory fake): - DELETE /sc/ki -> unregister_ki(kb_id, ki_id) - PUT /sc/lease/renew -> renew_lease(kb_id) -> SmartConnectorLease - POST /sc/knowledge -> load_domain_knowledge(kb_id, knowledge) Models: - KnowledgeBaseInfo gains optional leaseRenewalTime (30-3600) and reasonerLevel (1-5) fields. - New SmartConnectorLease model for the lease-renewal response. - Client.register_kb now serialises with exclude_none=True so unset optional fields are omitted from the payload, per the spec. KnowledgeBase: - Adds renew_lease(), load_domain_knowledge(knowledge), and unregister_ki(ki_name) wrappers (name-based, resolves to KI id). - Constructor and KnowledgeBaseBuilder propagate the two new optional fields through to KnowledgeBaseInfo so they can be set via settings. Tests: - Adds unit tests for all three new client operations (success + not-found + unexpected-response paths where applicable). - Adds tests for the new KnowledgeBase wrappers via TestClient. Closes #39 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/knowledge_mapper/kb/builder.py | 2 + src/knowledge_mapper/kb/knowledge_base.py | 87 ++++++- src/knowledge_mapper/ke/client.py | 92 +++++++- src/knowledge_mapper/ke/models.py | 15 ++ src/knowledge_mapper/testing/fake_client.py | 45 +++- tests/test_client.py | 241 ++++++++++++++++++++ tests/test_kb_new_operations.py | 119 ++++++++++ 7 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 tests/test_kb_new_operations.py diff --git a/src/knowledge_mapper/kb/builder.py b/src/knowledge_mapper/kb/builder.py index 20ea2d3..821662d 100644 --- a/src/knowledge_mapper/kb/builder.py +++ b/src/knowledge_mapper/kb/builder.py @@ -36,6 +36,8 @@ def __init__(self, settings: KnowledgeBaseSettings) -> None: name=settings.knowledge_base.name, description=settings.knowledge_base.description, ke_url=settings.knowledge_engine_endpoint, + lease_renewal_time=settings.knowledge_base.lease_renewal_time, + reasoner_level=settings.knowledge_base.reasoner_level, ) self._unhandled_incoming: set[str] = { ki.name diff --git a/src/knowledge_mapper/kb/knowledge_base.py b/src/knowledge_mapper/kb/knowledge_base.py index 04f51c0..8cd3481 100644 --- a/src/knowledge_mapper/kb/knowledge_base.py +++ b/src/knowledge_mapper/kb/knowledge_base.py @@ -19,6 +19,7 @@ KnowledgeBaseInfo, KnowledgeInteractionInfo, PostReactInteractionInfo, + SmartConnectorLease, ) from ..knowledge_interaction import ( Handler, @@ -44,7 +45,15 @@ class KnowledgeBase: Starts in unregistered state. """ - def __init__(self, id: str, name: str, description: str, ke_url: str): + def __init__( + self, + id: str, + name: str, + description: str, + ke_url: str, + lease_renewal_time: int | None = None, + reasoner_level: int | None = None, + ): self.state = KnowledgeBaseState.UNREGISTERED self.ki_registry: dict[str, KnowledgeInteractionContext[Any, ...]] = {} self._ki_registry_by_id: dict[str, KnowledgeInteractionContext[Any, ...]] = {} @@ -53,6 +62,8 @@ def __init__(self, id: str, name: str, description: str, ke_url: str): id=id, name=name, description=description, + lease_renewal_time=lease_renewal_time, + reasoner_level=reasoner_level, ) self.dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] = {} @@ -268,6 +279,80 @@ async def sync_knowledge_interactions(self) -> None: self._ki_registry_by_id[ki_ctx.info.id] = ki_ctx return + async def unregister_ki(self, ki_name: str) -> None: + """Unregister a single knowledge interaction by name from this KB at the KE + runtime, and remove it from this object's local registry. + + Raises: + ValueError: If the KB is not registered, or if ``ki_name`` is unknown, or + if the KI is not currently registered at the KE runtime. + SmartConnectorNotFoundError: If the KB's smart connector or the KI is not + found in the KE runtime. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + if self.state != KnowledgeBaseState.REGISTERED: + raise ValueError( + f"Cannot unregister KI '{ki_name}' because the KB is not registered." + ) + if ki_name not in self.ki_registry: + raise ValueError( + f"Cannot unregister KI '{ki_name}': no KI with that name is " + f"registered for this KB." + ) + ki_ctx = self.ki_registry[ki_name] + if ( + ki_ctx.status != KnowledgeInteractionStatus.REGISTERED + or ki_ctx.info.id is None + ): + raise ValueError( + f"Cannot unregister KI '{ki_name}' because it is not currently " + f"registered at the KE runtime." + ) + + logger.info("Unregistering KI '%s' (%s).", ki_name, ki_ctx.info.id) + await self.client.unregister_ki(kb_id=self.info.id, ki_id=ki_ctx.info.id) + self._ki_registry_by_id.pop(ki_ctx.info.id, None) + self.ki_registry.pop(ki_name, None) + + async def renew_lease(self) -> SmartConnectorLease: + """Renew this KB's smart connector lease at the KE runtime and return the new + lease. + + Raises: + ValueError: If the KB is not registered. + SmartConnectorNotFoundError: If the KB's smart connector is not found in + the KE runtime, or it does not have a lease. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + if self.state != KnowledgeBaseState.REGISTERED: + raise ValueError("Cannot renew lease because the KB is not registered.") + logger.debug("Renewing lease for KB '%s'.", self.info.id) + return await self.client.renew_lease(kb_id=self.info.id) + + async def load_domain_knowledge(self, knowledge: str) -> None: + """Load domain knowledge (Apache Jena facts/rules) into this KB's smart + connector. Replaces any previously loaded domain knowledge. + + Args: + knowledge: The domain knowledge (facts and rules) as plain text in the + Apache Jena Rules syntax. + + Raises: + ValueError: If the KB is not registered. + SmartConnectorNotFoundError: If the KB's smart connector is not found in + the KE runtime. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + if self.state != KnowledgeBaseState.REGISTERED: + raise ValueError( + "Cannot load domain knowledge because the KB is not registered." + ) + logger.debug("Loading domain knowledge for KB '%s'.", self.info.id) + await self.client.load_domain_knowledge(kb_id=self.info.id, knowledge=knowledge) + def ki_from_info( self, info: KnowledgeInteractionInfo, diff --git a/src/knowledge_mapper/ke/client.py b/src/knowledge_mapper/ke/client.py index 8ab43eb..a74f095 100644 --- a/src/knowledge_mapper/ke/client.py +++ b/src/knowledge_mapper/ke/client.py @@ -16,6 +16,7 @@ KnowledgeInteractionInfo, PostReactInteractionInfo, PostResult, + SmartConnectorLease, ) logger = logging.getLogger(__name__) @@ -122,6 +123,48 @@ async def register_ki( """ ... + async def unregister_ki(self, kb_id: str, ki_id: str) -> None: + """Unregister a single knowledge interaction with the given ID from the KB + with the given ID. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID, or no knowledge interaction with the given ID exists for that KB. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + async def renew_lease(self, kb_id: str) -> SmartConnectorLease: + """Renew the lease of the smart connector for the given KB and return the + new lease. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID, or it does not have a lease. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + async def load_domain_knowledge(self, kb_id: str, knowledge: str) -> None: + """Load domain knowledge (Apache Jena facts/rules) into the smart connector + for the given KB. Replaces any previously loaded domain knowledge. + + Args: + kb_id: The ID of the KB whose smart connector should be loaded with the + given domain knowledge. + knowledge: The domain knowledge (both facts and rules) as plain text in + the Apache Jena Rules syntax. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + async def poll_ki_call(self, kb_id: str) -> tuple[PollResult, HandleRequest | None]: """Poll the KE runtime for an incoming KI call for the given KB. @@ -247,7 +290,7 @@ async def register_kb( logger.debug("Registering knowledge base '%s' at %s.", info.id, self.ke_url) response = await self._http.post( f"{self.ke_url}/sc", - json=info.model_dump(by_alias=True), + json=info.model_dump(by_alias=True, exclude_none=True), ) if not response.is_success: raise UnexpectedHttpResponseError(response) @@ -309,6 +352,53 @@ async def register_ki( ) return registered_ki + async def unregister_ki(self, kb_id: str, ki_id: str) -> None: + logger.debug( + "Unregistering knowledge interaction '%s' for KB '%s' at %s.", + ki_id, + kb_id, + self.ke_url, + ) + response = await self._http.delete( + f"{self.ke_url}/sc/ki", + headers={ + "Knowledge-Base-Id": kb_id, + "Knowledge-Interaction-Id": ki_id, + }, + ) + if response.status_code == 404: + raise SmartConnectorNotFoundError(kb_id, self.ke_url) + if not response.is_success: + raise UnexpectedHttpResponseError(response) + + async def renew_lease(self, kb_id: str) -> SmartConnectorLease: + logger.debug("Renewing lease for KB '%s' at %s.", kb_id, self.ke_url) + response = await self._http.put( + f"{self.ke_url}/sc/lease/renew", + headers={"Knowledge-Base-Id": kb_id}, + ) + if response.status_code == 404: + raise SmartConnectorNotFoundError(kb_id, self.ke_url) + if not response.is_success: + raise UnexpectedHttpResponseError(response) + + return SmartConnectorLease.model_validate(response.json()) + + async def load_domain_knowledge(self, kb_id: str, knowledge: str) -> None: + logger.debug("Loading domain knowledge for KB '%s' at %s.", kb_id, self.ke_url) + response = await self._http.post( + f"{self.ke_url}/sc/knowledge", + content=knowledge.encode("utf-8"), + headers={ + "Knowledge-Base-Id": kb_id, + "Content-Type": "text/plain; charset=UTF-8", + }, + ) + if response.status_code == 404: + raise SmartConnectorNotFoundError(kb_id, self.ke_url) + if not response.is_success: + raise UnexpectedHttpResponseError(response) + async def poll_ki_call(self, kb_id: str) -> tuple[PollResult, HandleRequest | None]: logger.debug("Polling for KI calls...") response = await self._http.get( diff --git a/src/knowledge_mapper/ke/models.py b/src/knowledge_mapper/ke/models.py index 87e21b9..534fc92 100644 --- a/src/knowledge_mapper/ke/models.py +++ b/src/knowledge_mapper/ke/models.py @@ -114,6 +114,21 @@ class KnowledgeBaseInfo(BaseModel): id: Annotated[str, Field(..., alias="knowledgeBaseId")] name: Annotated[str, Field(..., alias="knowledgeBaseName")] description: Annotated[str, Field(..., alias="knowledgeBaseDescription")] + lease_renewal_time: Annotated[ + int | None, Field(default=None, alias="leaseRenewalTime", ge=30, le=3600) + ] = None + reasoner_level: Annotated[ + int | None, Field(default=None, alias="reasonerLevel", ge=1, le=5) + ] = None + + +class SmartConnectorLease(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, frozen=True, populate_by_name=True + ) + + knowledge_base_id: str + expires: datetime class KiTypes(StrEnum): diff --git a/src/knowledge_mapper/testing/fake_client.py b/src/knowledge_mapper/testing/fake_client.py index aa79ed0..02aa4f1 100644 --- a/src/knowledge_mapper/testing/fake_client.py +++ b/src/knowledge_mapper/testing/fake_client.py @@ -1,9 +1,10 @@ """In-memory FakeClient that satisfies ClientProtocol for use in tests.""" import asyncio -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from knowledge_mapper.ke.client import ClientProtocol, HandleRequest, PollResult +from knowledge_mapper.ke.errors import SmartConnectorNotFoundError from knowledge_mapper.ke.models import ( AskResult, BindingSet, @@ -12,6 +13,7 @@ KnowledgeBaseInfo, KnowledgeInteractionInfo, PostResult, + SmartConnectorLease, ) @@ -31,6 +33,10 @@ def __init__(self, fake_url) -> None: asyncio.Queue() ) self._next_handle_request_id: int = 1 + # Maps kb_id -> the most recently loaded domain knowledge string. + self._domain_knowledge: dict[str, str] = {} + # Maps kb_id -> number of times its lease has been renewed. + self._lease_renewals: dict[str, int] = {} async def ke_is_available(self) -> bool: return True @@ -72,6 +78,43 @@ async def register_ki( self._knowledge_interactions.setdefault(kb_id, []).append(registered) return registered + async def unregister_ki(self, kb_id: str, ki_id: str) -> None: + if kb_id not in self._knowledge_bases: + raise SmartConnectorNotFoundError(kb_id, self._ke_url) + kis = self._knowledge_interactions.get(kb_id, []) + for i, ki in enumerate(kis): + if ki.id == ki_id: + kis.pop(i) + return + raise SmartConnectorNotFoundError(kb_id, self._ke_url) + + async def renew_lease(self, kb_id: str) -> SmartConnectorLease: + if kb_id not in self._knowledge_bases: + raise SmartConnectorNotFoundError(kb_id, self._ke_url) + self._lease_renewals[kb_id] = self._lease_renewals.get(kb_id, 0) + 1 + info = self._knowledge_bases[kb_id] + # Default to 60 seconds if leaseRenewalTime is unset. + renewal_seconds = info.lease_renewal_time or 60 + return SmartConnectorLease( + knowledge_base_id=kb_id, + expires=datetime.now(tz=UTC) + timedelta(seconds=renewal_seconds), + ) + + async def load_domain_knowledge(self, kb_id: str, knowledge: str) -> None: + if kb_id not in self._knowledge_bases: + raise SmartConnectorNotFoundError(kb_id, self._ke_url) + self._domain_knowledge[kb_id] = knowledge + + @property + def loaded_domain_knowledge(self) -> dict[str, str]: + """Return the most-recently-loaded domain knowledge per KB id (for tests).""" + return dict(self._domain_knowledge) + + @property + def lease_renewals(self) -> dict[str, int]: + """Return the number of lease renewals per KB id (for tests).""" + return dict(self._lease_renewals) + async def poll_ki_call(self, kb_id: str) -> tuple[PollResult, HandleRequest | None]: return await self._incoming_calls.get() diff --git a/tests/test_client.py b/tests/test_client.py index 157122c..83f2f0e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,11 +3,16 @@ import pytest from knowledge_mapper.ke import Client +from knowledge_mapper.ke.errors import ( + SmartConnectorNotFoundError, + UnexpectedHttpResponseError, +) from knowledge_mapper.ke.models import ( AskAnswerInteractionInfo, KnowledgeBaseInfo, KnowledgeInteractionInfo, PostReactInteractionInfo, + SmartConnectorLease, ) @@ -178,3 +183,239 @@ async def test_register_knowledge_interaction(client: Client): ) assert registered_ki.id == "http://example.org/test#kb/interaction/ask-interaction" + + +async def test_register_knowledge_base_with_optional_fields(client: Client): + """leaseRenewalTime / reasonerLevel are sent only when set (no None in payload).""" + mock_get_response = MagicMock() + mock_get_response.status_code = 404 + + mock_post_response = MagicMock() + mock_post_response.is_success = True + + with ( + patch.object( + client._http, + "get", + new_callable=AsyncMock, + return_value=mock_get_response, + ), + patch.object( + client._http, + "post", + new_callable=AsyncMock, + return_value=mock_post_response, + ) as mock_post, + ): + await client.register_kb( + info=KnowledgeBaseInfo( + id="http://example.org/test#kb", + name="test-kb", + description="A KB for testing.", + lease_renewal_time=60, + reasoner_level=4, + ) + ) + + mock_post.assert_called_once_with( + "http://fake-ke/sc", + json={ + "knowledgeBaseId": "http://example.org/test#kb", + "knowledgeBaseName": "test-kb", + "knowledgeBaseDescription": "A KB for testing.", + "leaseRenewalTime": 60, + "reasonerLevel": 4, + }, + ) + + +async def test_register_knowledge_base_omits_unset_optional_fields(client: Client): + """Optional fields must NOT appear in the request payload when unset.""" + mock_get_response = MagicMock() + mock_get_response.status_code = 404 + + mock_post_response = MagicMock() + mock_post_response.is_success = True + + with ( + patch.object( + client._http, + "get", + new_callable=AsyncMock, + return_value=mock_get_response, + ), + patch.object( + client._http, + "post", + new_callable=AsyncMock, + return_value=mock_post_response, + ) as mock_post, + ): + await client.register_kb( + info=KnowledgeBaseInfo( + id="http://example.org/test#kb", + name="test-kb", + description="A KB for testing.", + ) + ) + + payload = mock_post.call_args.kwargs["json"] + assert "leaseRenewalTime" not in payload + assert "reasonerLevel" not in payload + + +async def test_unregister_knowledge_interaction(client: Client): + mock_response = MagicMock() + mock_response.is_success = True + mock_response.status_code = 200 + + with patch.object( + client._http, + "delete", + new_callable=AsyncMock, + return_value=mock_response, + ) as mock_delete: + await client.unregister_ki( + kb_id="http://example.org/test#kb", + ki_id="http://example.org/test#kb/interaction/ask-interaction", + ) + + mock_delete.assert_called_once_with( + "http://fake-ke/sc/ki", + headers={ + "Knowledge-Base-Id": "http://example.org/test#kb", + "Knowledge-Interaction-Id": ( + "http://example.org/test#kb/interaction/ask-interaction" + ), + }, + ) + + +async def test_unregister_knowledge_interaction_not_found(client: Client): + mock_response = MagicMock() + mock_response.is_success = False + mock_response.status_code = 404 + + with ( + patch.object( + client._http, + "delete", + new_callable=AsyncMock, + return_value=mock_response, + ), + pytest.raises(SmartConnectorNotFoundError), + ): + await client.unregister_ki( + kb_id="http://example.org/missing#kb", + ki_id="http://example.org/missing#kb/interaction/x", + ) + + +async def test_unregister_knowledge_interaction_unexpected_response(client: Client): + mock_response = MagicMock() + mock_response.is_success = False + mock_response.status_code = 500 + + with ( + patch.object( + client._http, + "delete", + new_callable=AsyncMock, + return_value=mock_response, + ), + pytest.raises(UnexpectedHttpResponseError), + ): + await client.unregister_ki( + kb_id="http://example.org/test#kb", + ki_id="http://example.org/test#kb/interaction/x", + ) + + +async def test_renew_lease(client: Client): + mock_response = MagicMock() + mock_response.is_success = True + mock_response.status_code = 200 + mock_response.json.return_value = { + "knowledgeBaseId": "http://example.org/test#kb", + "expires": "2026-06-25T12:00:00+00:00", + } + + with patch.object( + client._http, + "put", + new_callable=AsyncMock, + return_value=mock_response, + ) as mock_put: + lease = await client.renew_lease("http://example.org/test#kb") + + mock_put.assert_called_once_with( + "http://fake-ke/sc/lease/renew", + headers={"Knowledge-Base-Id": "http://example.org/test#kb"}, + ) + assert isinstance(lease, SmartConnectorLease) + assert lease.knowledge_base_id == "http://example.org/test#kb" + + +async def test_renew_lease_not_found(client: Client): + mock_response = MagicMock() + mock_response.is_success = False + mock_response.status_code = 404 + + with ( + patch.object( + client._http, + "put", + new_callable=AsyncMock, + return_value=mock_response, + ), + pytest.raises(SmartConnectorNotFoundError), + ): + await client.renew_lease("http://example.org/missing#kb") + + +async def test_load_domain_knowledge(client: Client): + mock_response = MagicMock() + mock_response.is_success = True + mock_response.status_code = 200 + + knowledge = "-> ( saref:Sensor rdfs:subClassOf saref:Device ) ." + + with patch.object( + client._http, + "post", + new_callable=AsyncMock, + return_value=mock_response, + ) as mock_post: + await client.load_domain_knowledge( + kb_id="http://example.org/test#kb", + knowledge=knowledge, + ) + + mock_post.assert_called_once_with( + "http://fake-ke/sc/knowledge", + content=knowledge.encode("utf-8"), + headers={ + "Knowledge-Base-Id": "http://example.org/test#kb", + "Content-Type": "text/plain; charset=UTF-8", + }, + ) + + +async def test_load_domain_knowledge_not_found(client: Client): + mock_response = MagicMock() + mock_response.is_success = False + mock_response.status_code = 404 + + with ( + patch.object( + client._http, + "post", + new_callable=AsyncMock, + return_value=mock_response, + ), + pytest.raises(SmartConnectorNotFoundError), + ): + await client.load_domain_knowledge( + kb_id="http://example.org/missing#kb", + knowledge="-> ( a b c ) .", + ) diff --git a/tests/test_kb_new_operations.py b/tests/test_kb_new_operations.py new file mode 100644 index 0000000..83170e8 --- /dev/null +++ b/tests/test_kb_new_operations.py @@ -0,0 +1,119 @@ +"""Tests for KnowledgeBase wrappers around the three new client operations: + +- ``unregister_ki(ki_name)`` +- ``renew_lease()`` +- ``load_domain_knowledge(knowledge)`` +""" + +import pytest + +from knowledge_mapper import KnowledgeBase +from knowledge_mapper.ke.errors import SmartConnectorNotFoundError +from knowledge_mapper.ke.models import ( + KnowledgeInteractionInfo, + SmartConnectorLease, +) +from knowledge_mapper.testing import TestClient + + +def _kb() -> KnowledgeBase: + kb = KnowledgeBase( + id="http://example.org/test#kb", + name="test-kb", + description="A KB for testing.", + ke_url="http://fake-ke", + ) + kb.client = TestClient(fake_url="http://fake-ke") + return kb + + +async def test_unregister_ki_removes_from_registries_and_calls_client(): + kb = _kb() + await kb.register() + await kb.register_ki( + ki_ctx=_ki_ctx("ask-it"), + ) + assert "ask-it" in kb.ki_registry + ki_id = kb.ki_registry["ask-it"].info.id + assert ki_id is not None and ki_id in kb._ki_registry_by_id + + await kb.unregister_ki("ask-it") + + assert "ask-it" not in kb.ki_registry + assert ki_id not in kb._ki_registry_by_id + assert await kb.client.get_all_knowledge_interactions(kb.info.id) == [] + + +async def test_unregister_ki_raises_when_kb_not_registered(): + kb = _kb() + with pytest.raises(ValueError, match="not registered"): + await kb.unregister_ki("ask-it") + + +async def test_unregister_ki_raises_for_unknown_ki(): + kb = _kb() + await kb.register() + with pytest.raises(ValueError, match="no KI with that name"): + await kb.unregister_ki("does-not-exist") + + +async def test_renew_lease_returns_lease_and_invokes_client(): + kb = _kb() + await kb.register() + + lease = await kb.renew_lease() + + assert isinstance(lease, SmartConnectorLease) + assert lease.knowledge_base_id == kb.info.id + # The TestClient tracks how often renew_lease was called per KB id. + assert kb.client.lease_renewals[kb.info.id] == 1 # pyright: ignore[reportAttributeAccessIssue] + + +async def test_renew_lease_unknown_kb_raises(): + kb = _kb() + await kb.register() + # Simulate the KB disappearing from the runtime. + await kb.client.unregister_kb(kb.info.id) + with pytest.raises(SmartConnectorNotFoundError): + await kb.renew_lease() + + +async def test_renew_lease_when_kb_not_registered_raises(): + kb = _kb() + with pytest.raises(ValueError, match="not registered"): + await kb.renew_lease() + + +async def test_load_domain_knowledge_stores_payload(): + kb = _kb() + await kb.register() + + knowledge = "-> ( saref:Sensor rdfs:subClassOf saref:Device) ." + await kb.load_domain_knowledge(knowledge) + + assert kb.client.loaded_domain_knowledge[kb.info.id] == knowledge # pyright: ignore[reportAttributeAccessIssue] + + +async def test_load_domain_knowledge_when_kb_not_registered_raises(): + kb = _kb() + with pytest.raises(ValueError, match="not registered"): + await kb.load_domain_knowledge("-> ( a b c ) .") + + +def _ki_ctx(name: str): + """Build a registered ASK-style KI context for use in unregister tests.""" + from knowledge_mapper.ke.models import AskAnswerInteractionInfo, KiTypes + from knowledge_mapper.knowledge_interaction import ( + KnowledgeInteractionContext, + KnowledgeInteractionStatus, + ) + + return KnowledgeInteractionContext[KnowledgeInteractionInfo, ...]( + info=AskAnswerInteractionInfo( + type=KiTypes.ASK, + name=name, + graph_pattern="?s ?p ?o . ", + ), + handler=None, + status=KnowledgeInteractionStatus.UNREGISTERED, + ) From a88688f0fe9c3701dd22e8a124d464e00339694a Mon Sep 17 00:00:00 2001 From: david Date: Thu, 25 Jun 2026 16:53:02 +0200 Subject: [PATCH 2/2] Add to README and typing hint --- README.md | 2 +- docs/img/architecture.graphml | 467 ---------------------- docs/img/architecture.png | Bin 58913 -> 0 bytes src/knowledge_mapper/kb/knowledge_base.py | 2 +- 4 files changed, 2 insertions(+), 469 deletions(-) delete mode 100644 docs/img/architecture.graphml delete mode 100644 docs/img/architecture.png diff --git a/README.md b/README.md index 21ccf5f..e3b8f0e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The Knowledge Mapper is a Python SDK for connecting your applications to the [TNO Knowledge Engine (TKE)](https://docs.knowledge-engine.eu/) network. Define knowledge interactions with decorators, use typed binding models, and let the SDK handle registration, polling, and data exchange with the network. -![architecture diagram](./docs/img/architecture.png) +The best way to learn to work with the Knowledge Mapper is to check out the [examples](/examples/). Users that are familiar with Python's [FastAPI](https://fastapi.tiangolo.com/) will recognize many concepts from that package. ## Quick Start diff --git a/docs/img/architecture.graphml b/docs/img/architecture.graphml deleted file mode 100644 index 4c3515d..0000000 --- a/docs/img/architecture.graphml +++ /dev/null @@ -1,467 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TkeClient.py - - - - - - - - - - - KE runtime - - - - - - - - - - - KE REST API - - - - - - - - - - - Knowledge Mapper CLI - - - - - - - - - - - Inter KE runtime protocol - - - - - - - - - - - Other KE runtime - - - - - - - - - - - Java Developer API - - - - - - - - - - - TkeClient.js - - - - - - - - - - - TkeClient.java - - - - - - - - - - - Knowledge Directory protocol - - - - - - - - - - - Knowledge Directory - - - - - - - - - - - Other KE runtime - - - - - - - - - - - python -m knowledge_mapper config.jsonc - - - - - - - - - - - config.jsonc - - - - - - - - - - - { - "knowledge_engine_endpoint": "http://localhost:8280/rest", - "knowledge_base": { - "id": "https://example.org/a-sparql-knowledge-base", - "name": "Some SPARQL knowledge base", - "description": "This is just an example." - }, - "sparql": { - "endpoint": "http://localhost:3030/example" - }, - "knowledge_interactions": [ - { - "type": "answer", - "vars": ["a", "b"], - "pattern": "?a <https://example.org/isRelatedTo> ?b ." - } - ] -} - - - - - - - - - - - SPARQL - - - - - - - - - - - SQL - - - - - - - - - - - Python class - - - - - - - - - - - A - - - - - - - - - - - B - - - - - - - - - - - = - -A initiates -communication -with B - - - - - - - - - - - Defines mapping and configures -connection details for data source - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/architecture.png b/docs/img/architecture.png deleted file mode 100644 index 6370258ae8c4f83e5818d50912d4ab3eae9d2bc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58913 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 diff --git a/src/knowledge_mapper/kb/knowledge_base.py b/src/knowledge_mapper/kb/knowledge_base.py index 8cd3481..199d600 100644 --- a/src/knowledge_mapper/kb/knowledge_base.py +++ b/src/knowledge_mapper/kb/knowledge_base.py @@ -237,7 +237,7 @@ def wrapper( *args, **kwargs, ) -> BindingSet | Sequence[BindingModel]: - return func(binding_set, info, *args, **kwargs) + return func(binding_set, info, *args, **kwargs) # pyright: ignore[reportReturnType] self._register_ki_locally( KnowledgeInteractionContext(