Skip to content

feat(python-sdk): add logger, datastore queue parity, and docs refresh - #6

Merged
usmanabbas7 merged 1 commit into
mainfrom
codex/python-sdk-logger-datastore-docs
Mar 30, 2026
Merged

feat(python-sdk): add logger, datastore queue parity, and docs refresh#6
usmanabbas7 merged 1 commit into
mainfrom
codex/python-sdk-logger-datastore-docs

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Summary

This PR closes a few of the remaining parity gaps between the Python SDK and the JavaScript SDK by adding logger support, datastore queue behavior, helper utilities, and updated package/docs metadata.

What’s Included

  • Added logger support to the Python SDK runtime

    • introduced LogManager
    • added LogLevel and LogMethod enums
    • wired logger configuration through ConvertSDK
    • added support for custom logger clients and method maps
  • Added helper utilities matching the JS SDK support layer

    • FileLogger
    • DataStore
  • Reworked DataStoreManager to behave more like the JS SDK

    • queue-based datastore writes
    • queue release on batch size
    • queue release on timeout
    • merge queued writes by visitor key
    • fire DATA_STORE_QUEUE_RELEASED events on release
    • close/stop queue timers cleanly
  • Updated DataManager

    • datastore writes now use the async queue path when available
    • persisted datastore segments are filtered down to reportable segment keys, aligning more closely with JS behavior
  • Updated Core.close()

    • now shuts down datastore queue timers in addition to refresh/API timers
  • Refreshed public exports

    • exported logger types/utilities from the package root
  • Refreshed docs and metadata

    • updated README to reflect the current SDK capabilities
    • updated package description in pyproject.toml

Testing

Added and updated unit coverage for:

  • logger levels and custom method mapping
  • file logger output
  • file-backed datastore helper behavior
  • datastore queue release on size
  • datastore queue release on timeout
  • queued datastore merge behavior
  • event manager listener error logging
  • datastore timer shutdown during SDK close
  • filtered datastore persistence from DataManager

Verification:

  • .venv/bin/pytest -q
  • 85 passed

Why This PR

The Python SDK already had the core evaluation flow and public SDK surface in place, but it was still missing part of the JS support/runtime layer. This PR brings the Python implementation closer to the JavaScript SDK in the areas of logging, queued datastore persistence, and developer-facing helper utilities.

@usmanabbas7
usmanabbas7 merged commit 4449c72 into main Mar 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions the Python SDK from a core-only implementation to a full-featured port of the Convert Fullstack SDK. It introduces a multi-client logging system, visitor persistence with asynchronous queuing, and utilities for file-based data storage. Review feedback highlights a logic error in segment filtering that could lead to persisting non-reportable data, thread-safety concerns in the file-based data store, and opportunities for performance optimization through bulk updates. Additionally, it is recommended to remove camelCase method aliases to maintain PEP 8 compliance.

Comment on lines +105 to +112
stored_value = (
object_deep_merge(
data,
{"segments": {**report_segments, **new_segments}},
)
if new_segments
else updated
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a potential bug here. If new_data contains only non-reportable segments, new_segments will be empty, and the else branch will be taken. This sets stored_value to updated, which includes the non-reportable segments. This seems to contradict the goal of persisting only reportable segments. The stored_value should always be constructed using only filtered, reportable segments.

            stored_value = object_deep_merge(
                data,
                {"segments": {**report_segments, **new_segments}},
            )

Comment on lines +89 to +90
for key, value in queued_items.items():
self.set(key, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling set in a loop for each item can be inefficient, causing repeated file reads and writes if using a file-based datastore like the DataStore utility. To optimize this, you can perform a bulk update if the underlying data_store supports it (e.g., with a set_many method).

Suggested change
for key, value in queued_items.items():
self.set(key, value)
if hasattr(self.data_store, "set_many") and callable(
getattr(self.data_store, "set_many")
):
try:
self.data_store.set_many(queued_items)
except Exception as error:
if self._logger_manager:
self._logger_manager.error(
"DataStoreManager.release_queue() bulk set failed",
{"error": str(error)},
)
else:
for key, value in queued_items.items():
self.set(key, value)

Comment on lines +8 to +32
class DataStore:
def __init__(self, file_path: str) -> None:
self._file_path = Path(file_path)
self._file_path.parent.mkdir(parents=True, exist_ok=True)
if not self._file_path.exists():
self._file_path.write_text("{}", encoding="utf-8")

def _read(self) -> dict[str, Any]:
try:
return json.loads(self._file_path.read_text(encoding="utf-8"))
except Exception:
return {}

def get(self, key: str) -> Any:
return self._read().get(key)

def set(self, key: str, value: Any) -> None:
data = self._read()
data[key] = value
self._file_path.write_text(json.dumps(data), encoding="utf-8")

def delete(self, key: str) -> None:
data = self._read()
data.pop(key, None)
self._file_path.write_text(json.dumps(data), encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This DataStore implementation has a couple of areas for improvement regarding thread safety and performance.

  • Thread Safety: The read-modify-write operations in set and delete are not atomic. If multiple threads call these methods concurrently, it can lead to race conditions and data loss.
  • Performance: DataStoreManager calls set in a loop, which is inefficient as it reads and writes the entire file for each key.

I suggest adding a threading.Lock to ensure thread safety, a set_many method for efficient bulk updates, and using more specific exceptions in _read.

import json
import threading
from collections.abc import Mapping
from pathlib import Path
from typing import Any


class DataStore:
    def __init__(self, file_path: str) -> None:
        self._file_path = Path(file_path)
        self._lock = threading.Lock()
        self._file_path.parent.mkdir(parents=True, exist_ok=True)
        if not self._file_path.exists():
            with self._lock:
                if not self._file_path.exists():
                    self._file_path.write_text("{}", encoding="utf-8")

    def _read(self) -> dict[str, Any]:
        try:
            return json.loads(self._file_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, FileNotFoundError):
            return {}

    def get(self, key: str) -> Any:
        with self._lock:
            return self._read().get(key)

    def set(self, key: str, value: Any) -> None:
        with self._lock:
            data = self._read()
            data[key] = value
            self._file_path.write_text(json.dumps(data), encoding="utf-8")

    def set_many(self, items: Mapping[str, Any]) -> None:
        with self._lock:
            data = self._read()
            data.update(items)
            self._file_path.write_text(json.dumps(data), encoding="utf-8")

    def delete(self, key: str) -> None:
        with self._lock:
            data = self._read()
            data.pop(key, None)
            self._file_path.write_text(json.dumps(data), encoding="utf-8")

Comment on lines +97 to +98
def releaseQueue(self, reason: str | None = None) -> None:
self.release_queue(reason)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While I understand the goal of achieving parity with the JS SDK, adding camelCase aliases for Python methods goes against PEP 8, which recommends snake_case for functions and methods. This can be confusing for Python developers and harm maintainability. It's best to stick to idiomatic Python and remove these aliases.

Comment on lines +107 to +113
def addClient(
self,
client: Any | None = None,
level: LogLevel | str = LogLevel.TRACE,
methodMap: dict[str, str] | None = None,
) -> None:
self.add_client(client, level, methodMap)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These camelCase aliases (addClient, and setClientLevel below) violate Python's PEP 8 style guide. For better readability and consistency within the Python ecosystem, I recommend removing them and using only the snake_case versions.

Comment on lines +23 to +24
except Exception as error:
print(error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception can hide unexpected errors. It's better to catch more specific exceptions related to file I/O, such as IOError or OSError.

Suggested change
except Exception as error:
print(error)
except (IOError, OSError) as error:
print(error)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant