Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Knowledge and checkpoint loaders no longer crash on `Z`-suffixed timestamps
on Python 3.10.** `datetime.fromisoformat` did not accept a trailing `Z` (UTC
designator) until Python 3.11, so a timestamp such as `"2026-06-15T12:00:00Z"`
— the form Postgres/Supabase/Redis routinely return — raised `ValueError` on
the project's minimum interpreter. All 17 ISO-parse sites now route through a
single `selectools._time.parse_iso` helper that normalizes the suffix first.
(#136)
- **`selectools doctor` now prints the API-key status text it computes.** The
per-key line built a `"set" / "not set"` string and then dropped it, printing
only the `OK` / `MISSING` icon; it now shows both (e.g. `OPENAI_API_KEY: OK
(set)`). (#135)

### Internal

- **Removed confirmed dead code** (#137): three unused `serve` request/response
dataclasses (`InvokeRequest`, `BatchRequest`, `BatchResponse`), two unreachable
private helpers (`bm25._score_document`, `hybrid._find_matching_key`), four
dead local variables, and 104 unused imports across the `src/` tree. No public
symbols affected.
- **Tightened ruff config** (#138): `F401` (unused imports) and `F841` (unused
locals) are now enforced everywhere; the `F401` ignore is scoped to
`**/__init__.py` re-export hubs, so dead imports and locals can no longer
accumulate silently.
- The vestigial `PineconeVectorStore(environment=...)` parameter is now
documented as deprecated/ignored (Pinecone v3+ infers the host); removal is
tracked for a future minor (#149).

## [1.0.0] - 2026-06-15 — Stable

selectools is **1.0**. The public API is frozen: every public symbol carries a
Expand Down
29 changes: 29 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Knowledge and checkpoint loaders no longer crash on `Z`-suffixed timestamps
on Python 3.10.** `datetime.fromisoformat` did not accept a trailing `Z` (UTC
designator) until Python 3.11, so a timestamp such as `"2026-06-15T12:00:00Z"`
— the form Postgres/Supabase/Redis routinely return — raised `ValueError` on
the project's minimum interpreter. All 17 ISO-parse sites now route through a
single `selectools._time.parse_iso` helper that normalizes the suffix first.
(#136)
- **`selectools doctor` now prints the API-key status text it computes.** The
per-key line built a `"set" / "not set"` string and then dropped it, printing
only the `OK` / `MISSING` icon; it now shows both (e.g. `OPENAI_API_KEY: OK
(set)`). (#135)

### Internal

- **Removed confirmed dead code** (#137): three unused `serve` request/response
dataclasses (`InvokeRequest`, `BatchRequest`, `BatchResponse`), two unreachable
private helpers (`bm25._score_document`, `hybrid._find_matching_key`), four
dead local variables, and 104 unused imports across the `src/` tree. No public
symbols affected.
- **Tightened ruff config** (#138): `F401` (unused imports) and `F841` (unused
locals) are now enforced everywhere; the `F401` ignore is scoped to
`**/__init__.py` re-export hubs, so dead imports and locals can no longer
accumulate silently.
- The vestigial `PineconeVectorStore(environment=...)` parameter is now
documented as deprecated/ignored (Pinecone v3+ infers the host); removal is
tracked for a future minor (#149).

## [1.0.0] - 2026-06-15 — Stable

selectools is **1.0**. The public API is frozen: every public symbol carries a
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,16 @@ ignore = [
"E501", # line too long (handled by formatter)
"E402", # module-level import not at top
"E721", # type comparison with == (allowed; legacy test pattern)
"F401", # unused imports (common in __init__.py re-exports)
"F541", # f-string without placeholder
"F841", # local variable assigned but never used
"B011", # do not call assert False
"B023", # function definition does not bind loop variable
"B904", # raise without `from` in except (TODO: fix pre-existing violations)
]

[tool.ruff.lint.per-file-ignores]
# F401 unused-import is expected in __init__.py re-export hubs (the public-API
# surface). Everywhere else, unused imports and unused locals are real findings.
"**/__init__.py" = ["F401"]
"tests/*" = ["B"]
"examples/*" = ["B", "F"]
"scripts/*" = ["B", "F"]
Expand Down
42 changes: 42 additions & 0 deletions src/selectools/_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Internal time helpers.

Centralizes ISO-8601 timestamp parsing so every persistence backend handles a
trailing ``Z`` (UTC designator) the same way.

``datetime.fromisoformat`` did not accept the ``Z`` suffix until Python 3.11.
On Python 3.10 (the project's minimum) a ``Z``-suffixed timestamp such as
``"2026-06-15T12:00:00Z"`` raises ``ValueError``. Postgres/Supabase/Redis and
other stores routinely return that form, so parsing it inline crashed the
knowledge and checkpoint loaders on 3.10. ``parse_iso`` normalizes the suffix
before delegating, so the same code works on every supported interpreter.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Union


def parse_iso(value: Union[str, datetime]) -> datetime:
"""Parse an ISO-8601 timestamp, tolerating a trailing ``Z`` (UTC).

Accepts either a string or an already-parsed ``datetime`` (returned as-is,
so callers reading columns that may already be typed do not need their own
``isinstance`` guard). Timezone awareness is preserved exactly as encoded:
an offset-bearing string (including ``Z``) yields an aware datetime; a naive
string yields a naive datetime. Use :func:`ensure_aware` when a guaranteed
aware value is required.
"""
if isinstance(value, datetime):
return value
s = str(value)
if s.endswith("Z"):
s = s[:-1] + "+00:00"
return datetime.fromisoformat(s)


def ensure_aware(dt: datetime, *, tz: timezone = timezone.utc) -> datetime:
"""Attach ``tz`` (default UTC) to a naive datetime; pass aware ones through."""
if dt.tzinfo is None:
return dt.replace(tzinfo=tz)
return dt
2 changes: 1 addition & 1 deletion src/selectools/agent/_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import TYPE_CHECKING, Any, Optional, cast

if TYPE_CHECKING:
from ..memory import ConversationMemory
pass

logger = logging.getLogger(__name__)

Expand Down
1 change: 0 additions & 1 deletion src/selectools/agent/_memory_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ..types import Message, Role

if TYPE_CHECKING:
from ..memory import ConversationMemory
from .core import _RunContext


Expand Down
2 changes: 1 addition & 1 deletion src/selectools/agent/_provider_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple

# Module-level singleton for running sync provider calls in an async context.
# Creating a new ThreadPoolExecutor per call (inside a retry loop) wastes
Expand Down
4 changes: 2 additions & 2 deletions src/selectools/agent/_tool_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def _get_parallel_dispatch_executor() -> ThreadPoolExecutor:


from .._async_utils import run_in_executor_copyctx
from ..coherence import CoherenceResult, acheck_coherence, check_coherence
from ..policy import ApprovalRequest, PolicyDecision, PolicyResult, ToolPolicy
from ..coherence import acheck_coherence, check_coherence
from ..policy import ApprovalRequest, PolicyDecision, PolicyResult
from ..security import screen_output as screen_tool_output
from ..trace import StepType, TraceStep
from ..types import Message, Role
Expand Down
2 changes: 1 addition & 1 deletion src/selectools/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional, Union

from ..stability import beta, stable
from ..stability import stable

if TYPE_CHECKING:
from ..cache import Cache
Expand Down
5 changes: 1 addition & 4 deletions src/selectools/agent/config_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,20 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from ..stability import stable

if TYPE_CHECKING:
from ..cache import Cache
from ..cancellation import CancellationToken
from ..entity_memory import EntityMemory
from ..guardrails import GuardrailsPipeline
from ..knowledge import KnowledgeMemory
from ..knowledge_graph import KnowledgeGraphMemory
from ..observer import AgentObserver
from ..policy import ToolPolicy
from ..providers.base import Provider
from ..sessions import SessionStore
from ..unified_memory import UnifiedMemory
from ..usage import AgentUsage


@stable
Expand Down
2 changes: 1 addition & 1 deletion src/selectools/agent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..analytics import AgentAnalytics
from ..parser import ToolCallParser
from ..prompt import PromptBuilder
from ..providers.base import Provider, ProviderError
from ..providers.base import Provider
from ..providers.openai_provider import OpenAIProvider
from ..results import Artifact, _begin_artifact_collection
from ..stability import beta, deprecated, stable
Expand Down
1 change: 0 additions & 1 deletion src/selectools/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import json
import os
import threading
import time
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Optional
Expand Down
2 changes: 1 addition & 1 deletion src/selectools/cache_semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import math
import threading
import time
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, List, Optional, Tuple

from .cache import CacheStats
Expand Down
8 changes: 3 additions & 5 deletions src/selectools/checkpoint_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
import re
import threading
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from typing import List, Tuple

from ._time import parse_iso
from .orchestration.checkpoint import (
CheckpointMetadata,
_deserialize_checkpoint,
Expand Down Expand Up @@ -137,9 +137,7 @@ def list(self, graph_id: str) -> List[CheckpointMetadata]:
step=r[2],
node_name=r[3],
interrupted=r[4],
created_at=(
r[5] if isinstance(r[5], datetime) else datetime.fromisoformat(str(r[5]))
),
created_at=parse_iso(r[5]),
)
for r in rows
]
Expand Down
3 changes: 1 addition & 2 deletions src/selectools/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ def extract(data: dict, field: str) -> str: ...

from __future__ import annotations

from typing import Any, Callable, List, Optional, Sequence
from typing import Any, Callable, List, Optional

from .stability import beta
from .tools.base import Tool
from .tools.decorators import tool as tool_decorator


@beta
Expand Down
2 changes: 1 addition & 1 deletion src/selectools/evals/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Any, List, Optional, Union

from ..stability import beta

Expand Down
6 changes: 1 addition & 5 deletions src/selectools/evals/pairwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

from __future__ import annotations

import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from .. import __version__
from ..agent import Agent
from ..stability import beta
from .evaluators import DEFAULT_EVALUATORS
from .report import EvalReport
from .suite import EvalSuite
from .types import CaseResult, CaseVerdict, EvalMetadata, TestCase
from .types import CaseResult, CaseVerdict, TestCase


@beta
Expand Down
6 changes: 1 addition & 5 deletions src/selectools/evals/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

from __future__ import annotations

import html
import json
import threading
import time
from http.server import HTTPServer, SimpleHTTPRequestHandler
from io import BytesIO
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Dict

from ..stability import beta
from .types import CaseVerdict


class _DashboardHandler(SimpleHTTPRequestHandler):
Expand Down
3 changes: 1 addition & 2 deletions src/selectools/evals/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

from __future__ import annotations

from typing import Any, List, Optional
from typing import List, Optional

from ..agent import Agent
from ..stability import beta
from .evaluators import (
ContainsEvaluator,
CustomEvaluator,
InjectionResistanceEvaluator,
JsonValidityEvaluator,
LengthEvaluator,
PerformanceEvaluator,
PIILeakEvaluator,
Expand Down
2 changes: 1 addition & 1 deletion src/selectools/guardrails/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import logging
from dataclasses import dataclass, field
from typing import List, Optional
from typing import List

from selectools.stability import stable

Expand Down
14 changes: 7 additions & 7 deletions src/selectools/knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import os
import sqlite3
import threading
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
Expand All @@ -45,6 +44,7 @@
runtime_checkable,
)

from ._time import parse_iso
from .knowledge_sanitizers import dedupe_against
from .stability import beta, register_stability, stable

Expand Down Expand Up @@ -291,10 +291,10 @@ def _load_all(self) -> List[KnowledgeEntry]:
continue
try:
d = json.loads(line)
created_at = datetime.fromisoformat(d["created_at"])
created_at = parse_iso(d["created_at"])
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
updated_at = datetime.fromisoformat(d["updated_at"])
updated_at = parse_iso(d["updated_at"])
if updated_at.tzinfo is None:
updated_at = updated_at.replace(tzinfo=timezone.utc)
entries.append(
Expand Down Expand Up @@ -467,10 +467,10 @@ def _init_db(self) -> None:
)

def _row_to_entry(self, row: tuple) -> KnowledgeEntry:
created_at = datetime.fromisoformat(row[6])
created_at = parse_iso(row[6])
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
updated_at = datetime.fromisoformat(row[7])
updated_at = parse_iso(row[7])
if updated_at.tzinfo is None:
updated_at = updated_at.replace(tzinfo=timezone.utc)
return KnowledgeEntry(
Expand Down Expand Up @@ -564,15 +564,15 @@ def prune(
if persistent:
continue
if ttl is not None:
created_dt = datetime.fromisoformat(created)
created_dt = parse_iso(created)
if created_dt.tzinfo is None:
created_dt = created_dt.replace(tzinfo=timezone.utc)
if now > created_dt + timedelta(days=ttl):
conn.execute("DELETE FROM knowledge WHERE id = ?", (row_id,))
removed += 1
continue
if max_age_days is not None:
created_dt = datetime.fromisoformat(created)
created_dt = parse_iso(created)
if created_dt.tzinfo is None:
created_dt = created_dt.replace(tzinfo=timezone.utc)
if now > created_dt + timedelta(days=max_age_days):
Expand Down
Loading