From 1fbcb739c9c19dbca64b672ff389b58a2feb1b90 Mon Sep 17 00:00:00 2001 From: fatkobra <55045047+fatkobra@users.noreply.github.com> Date: Fri, 8 May 2026 15:27:54 +0000 Subject: [PATCH] fix(kg): tighten temporal handling and cleanup --- mempalace/config.py | 12 +++++++++--- mempalace/knowledge_graph.py | 23 ----------------------- tests/conftest.py | 10 ++++++++++ tests/test_config.py | 4 ++++ 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/mempalace/config.py b/mempalace/config.py index dfeae63..ab478ec 100644 --- a/mempalace/config.py +++ b/mempalace/config.py @@ -93,14 +93,16 @@ def sanitize_kg_value(value: str, field_name: str = "value") -> str: # Accepted: # YYYY-MM-DD # YYYY-MM-DDTHH:MM:SSZ +# YYYY-MM-DDTHH:MM:SS+00:00 (normalized to ...Z) # # Rejected: -# partial dates, naive datetimes, timezone offsets, fractional seconds, -# and SQLite-style space-separated datetimes. +# partial dates, naive datetimes, non-UTC timezone offsets, fractional +# seconds, and SQLite-style space-separated datetimes. _ISO_DATE_RE = re.compile(r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$") _ISO_UTC_DATETIME_RE = re.compile( - r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])" r"T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\dZ$" + r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])" + r"T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:Z|\+00:00)$" ) @@ -127,6 +129,7 @@ def sanitize_iso_temporal(value, field_name: str = "date"): - ``YYYY-MM-DD`` - ``YYYY-MM-DDTHH:MM:SSZ`` + - ``YYYY-MM-DDTHH:MM:SS+00:00`` normalized to ``...Z`` Partial dates are rejected because KG queries compare TEXT temporal values. Non-canonical datetime forms are rejected because mixed temporal string @@ -148,6 +151,9 @@ def sanitize_iso_temporal(value, field_name: str = "date"): "(expected YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ)" ) from None + if value.endswith("+00:00"): + value = f"{value[:-6]}Z" + return value diff --git a/mempalace/knowledge_graph.py b/mempalace/knowledge_graph.py index 8c3a749..774ee79 100644 --- a/mempalace/knowledge_graph.py +++ b/mempalace/knowledge_graph.py @@ -81,20 +81,6 @@ def _temporal_end_key(value: Optional[str]) -> Optional[str]: return value -def _triple_valid_at(valid_from: Optional[str], valid_to: Optional[str], as_of: str) -> bool: - as_of_key = _temporal_start_key(as_of) - valid_from_key = _temporal_start_key(valid_from) - valid_to_key = _temporal_end_key(valid_to) - - if valid_from_key is not None and valid_from_key > as_of_key: - return False - - if valid_to_key is not None and valid_to_key < as_of_key: - return False - - return True - - def _sql_temporal_start_expr(column: str) -> str: """SQLite expression for comparing valid_from-style temporal values.""" @@ -230,15 +216,6 @@ class KnowledgeGraph: self.close() return False - def __del__(self): - """Best-effort cleanup for callers/tests that forget to call close().""" - try: - self.close() - except Exception: - # Destructors must never raise, especially during interpreter - # shutdown when module globals may already be partially torn down. - pass - def _entity_id(self, name: str) -> str: return name.lower().replace(" ", "_").replace("'", "") diff --git a/tests/conftest.py b/tests/conftest.py index 4ed82ca..eb5c525 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,16 @@ def _reset_mcp_cache(): try: from mempalace import mcp_server + for kg in list(getattr(mcp_server, "_kg_by_path", {}).values()): + close = getattr(kg, "close", None) + if close is not None: + try: + close() + except Exception: + pass + if hasattr(mcp_server, "_kg_by_path"): + mcp_server._kg_by_path.clear() + mcp_server._client_cache = None mcp_server._collection_cache = None except (ImportError, AttributeError): diff --git a/tests/test_config.py b/tests/test_config.py index 93dacf3..faea345 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -341,3 +341,7 @@ def test_iso_temporal_rejects_invalid_calendar_date(): def test_iso_temporal_error_names_field(): with pytest.raises(ValueError, match="as_of"): sanitize_iso_temporal("2026-05-06T14:23", "as_of") + + +def test_iso_temporal_normalizes_plus_zero_offset_to_z(): + assert sanitize_iso_temporal("2026-05-06T14:23:00+00:00") == "2026-05-06T14:23:00Z"