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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# TraceDB Python SDK

[![PyPI](https://img.shields.io/pypi/v/tracedb)](https://pypi.org/project/tracedb/)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
[![Docs](https://img.shields.io/badge/docs-Python%20SDK-informational)](https://docs.trace-db.com/python-sdk)

TraceDB is an AI-native transactional candidate-stream database.
One logical record. One commit epoch. Many native views. No external sync
drift. Explain every candidate.
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ async = [
]

[project.urls]
Homepage = "https://github.com/Trace-DB/tracedb-python"
Homepage = "https://trace-db.com"
Documentation = "https://docs.trace-db.com/python-sdk"
Repository = "https://github.com/Trace-DB/tracedb-python"

[tool.setuptools]
Expand Down
96 changes: 96 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@

from tracedb import ( # noqa: E402
AsyncTraceDB,
AsyncTraceDBTable,
BatchPutResult,
HealthResponse,
PutResult,
RestoreResult,
TraceDB,
TraceDBHTTPError,
TraceDBRequestError,
TraceDBTable,
)


Expand Down Expand Up @@ -354,6 +357,35 @@ def fake_urlopen(request, timeout): # type: ignore[no-untyped-def]
self.assertEqual(body["limit"], 25)
self.assertEqual(body["cursor"], "25")

def test_restore_returns_typed_result_with_raw_mapping_access(self) -> None:
db = TraceDB("http://127.0.0.1:8090")
captured = []

def fake_urlopen(request, timeout): # type: ignore[no-untyped-def]
captured.append(request)
return _FakeResponse(
'{"status":"restored","restored":true,'
'"source":"/tmp/source","target":"/tmp/target"}'
)

with mock.patch("urllib.request.urlopen", side_effect=fake_urlopen):
response = db.restore("/tmp/source", "/tmp/target")

self.assertIsInstance(response, RestoreResult)
self.assertTrue(response.restored)
self.assertEqual(response.status, "restored")
self.assertEqual(response.source, "/tmp/source")
self.assertEqual(response.target, "/tmp/target")
self.assertEqual(response["target"], "/tmp/target")
self.assertEqual(len(captured), 1)
self.assertEqual(
captured[0].full_url, "http://127.0.0.1:8090/v1/admin/restore"
)
self.assertEqual(
json.loads(captured[0].data.decode("utf-8")),
{"source": "/tmp/source", "target": "/tmp/target"},
)

def test_query_builder_canonicalizes_allow_dirty_freshness(self) -> None:
db = TraceDB("http://127.0.0.1:8090")
captured = []
Expand Down Expand Up @@ -609,6 +641,62 @@ async def run() -> None:

asyncio.run(run())

@unittest.skipIf(
importlib.util.find_spec("aiohttp") is None,
"aiohttp optional async extra not installed",
)
def test_async_client_context_manager_closes_session(self) -> None:
async def run() -> None:
session = _FakeAsyncSession()
with mock.patch("aiohttp.ClientSession", return_value=session):
db = AsyncTraceDB(TraceDB("http://127.0.0.1:8090"))
async with db as entered:
self.assertIs(entered, db)
self.assertIs(db._session, session)

self.assertIsNone(db._session)
self.assertEqual(session.close_count, 1)

asyncio.run(run())

@unittest.skipIf(
importlib.util.find_spec("aiohttp") is None,
"aiohttp optional async extra not installed",
)
def test_async_table_uses_async_request_path(self) -> None:
async def run() -> None:
db = AsyncTraceDB(TraceDB("http://127.0.0.1:8090"))
try:
table = db.table("docs")
self.assertIsInstance(table, AsyncTraceDBTable)
self.assertNotIsInstance(table, TraceDBTable)
self.assertTrue(
asyncio.iscoroutinefunction(table.tenant("tenant-a").get)
)

with (
mock.patch(
"urllib.request.urlopen",
side_effect=AssertionError("sync table path used"),
) as urlopen,
mock.patch(
"aiohttp.ClientSession.request",
return_value=_FakeAsyncResponse('{"record":{"id":"intro"}}'),
) as request,
):
self.assertEqual(
await table.tenant("tenant-a").get("intro"),
{"record": {"id": "intro"}},
)

urlopen.assert_not_called()
request.assert_called_once()
finally:
if db._session is not None:
await db._session.close()

asyncio.run(run())

def test_safe_retries_do_not_retry_mutation_5xx(self) -> None:
db = TraceDB("http://127.0.0.1:8090", safe_retries=1)
retry_error = urllib.error.HTTPError(
Expand Down Expand Up @@ -868,6 +956,14 @@ async def text(self) -> str:
return self.body


class _FakeAsyncSession:
def __init__(self) -> None:
self.close_count = 0

async def close(self) -> None:
self.close_count += 1


class _FakeResponse:
status = 200

Expand Down
2 changes: 1 addition & 1 deletion tracedb-protocol.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
repo = "https://github.com/Trace-DB/tracedb-protocol"
revision = "833d3565dbc4cdc7cf678a64fca31204baa38f71"
revision = "7e4f606c4f8248eb3382916ae9c534da8a8c163d"
contract = "platform-contract-v0"
6 changes: 6 additions & 0 deletions tracedb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from ._version import __version__
from .client import (
AsyncTraceDB,
AsyncTraceDBQueryBuilder,
AsyncTraceDBTable,
TraceDB,
TraceDBHTTPError,
TraceDBQueryBuilder,
Expand All @@ -17,6 +19,7 @@
PutResult,
QueryResult,
ReadyResponse,
RestoreResult,
ScanResult,
SchemaApplyResult,
SnapshotResult,
Expand All @@ -26,6 +29,8 @@
"__version__",
# Client classes
"AsyncTraceDB",
"AsyncTraceDBQueryBuilder",
"AsyncTraceDBTable",
"TraceDB",
"TraceDBHTTPError",
"TraceDBQueryBuilder",
Expand All @@ -41,6 +46,7 @@
"PutResult",
"QueryResult",
"ReadyResponse",
"RestoreResult",
"SchemaApplyResult",
"ScanResult",
"SnapshotResult",
Expand Down
Loading
Loading