Skip to content

Commit 0d3812c

Browse files
authored
Add info method to logfire query clients (#1204)
1 parent 7bef828 commit 0d3812c

File tree

8 files changed

+263
-1
lines changed

8 files changed

+263
-1
lines changed

docs/how-to-guides/query-api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ Here's an example of how to use these clients:
8888
df_from_csv = pl.read_csv(StringIO(await client.query_csv(sql=query)))
8989
print(df_from_csv)
9090

91+
# Get read token info
92+
read_token_info = await client.info()
93+
print(read_token_info)
94+
9195

9296
if __name__ == '__main__':
9397
import asyncio
@@ -132,6 +136,10 @@ Here's an example of how to use these clients:
132136
df_from_csv = pl.read_csv(StringIO(client.query_csv(sql=query)))
133137
print(df_from_csv)
134138

139+
# Get read token info
140+
read_token_info = client.info()
141+
print(read_token_info)
142+
135143

136144
if __name__ == '__main__':
137145
main()

logfire-api/logfire_api/experimental/query_client.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from _typeshed import Incomplete
22
from datetime import datetime
3+
from uuid import UUID
34
from httpx import AsyncClient, Client, Response, Timeout
45
from httpx._client import BaseClient
56
from logfire._internal.config import get_base_url_from_token as get_base_url_from_token
@@ -15,6 +16,11 @@ class QueryExecutionError(RuntimeError):
1516
class QueryRequestError(RuntimeError):
1617
"""Raised when the query request is invalid."""
1718

19+
class ReadTokenInfo(TypedDict, total=True):
20+
"""Information about the read token."""
21+
organization_name: str
22+
project_name: str
23+
1824
class ColumnDetails(TypedDict):
1925
"""The details of a column in the row-oriented JSON-format query results."""
2026
name: str
@@ -49,6 +55,8 @@ class LogfireQueryClient(_BaseLogfireQueryClient[Client]):
4955
def __init__(self, read_token: str, base_url: str | None = None, timeout: Timeout = ..., **client_kwargs: Any) -> None: ...
5056
def __enter__(self) -> Self: ...
5157
def __exit__(self, exc_type: type[BaseException] | None = None, exc_value: BaseException | None = None, traceback: TracebackType | None = None) -> None: ...
58+
def info(self) -> ReadTokenInfo:
59+
"""Get information about the read token."""
5260
def query_json(self, sql: str, min_timestamp: datetime | None = None, max_timestamp: datetime | None = None, limit: int | None = None) -> QueryResults:
5361
"""Query Logfire data and return the results as a column-oriented dictionary."""
5462
def query_json_rows(self, sql: str, min_timestamp: datetime | None = None, max_timestamp: datetime | None = None, limit: int | None = None) -> RowQueryResults:
@@ -71,6 +79,8 @@ class AsyncLogfireQueryClient(_BaseLogfireQueryClient[AsyncClient]):
7179
def __init__(self, read_token: str, base_url: str | None = None, timeout: Timeout = ..., **async_client_kwargs: Any) -> None: ...
7280
async def __aenter__(self) -> Self: ...
7381
async def __aexit__(self, exc_type: type[BaseException] | None = None, exc_value: BaseException | None = None, traceback: TracebackType | None = None) -> None: ...
82+
async def info(self) -> ReadTokenInfo:
83+
"""Get information about the read token."""
7484
async def query_json(self, sql: str, min_timestamp: datetime | None = None, max_timestamp: datetime | None = None, limit: int | None = None) -> QueryResults:
7585
"""Query Logfire data and return the results as a column-oriented dictionary."""
7686
async def query_json_rows(self, sql: str, min_timestamp: datetime | None = None, max_timestamp: datetime | None = None, limit: int | None = None) -> RowQueryResults:

logfire/experimental/query_client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ class QueryRequestError(RuntimeError):
3232
pass
3333

3434

35+
class InfoRequestError(RuntimeError):
36+
"""Raised when the request for read token info fails because of unavailable information."""
37+
38+
pass
39+
40+
41+
class ReadTokenInfo(TypedDict, total=False):
42+
"""Information about the read token."""
43+
44+
organization_name: str
45+
project_name: str
46+
47+
3548
class ColumnDetails(TypedDict):
3649
"""The details of a column in the row-oriented JSON-format query results."""
3750

@@ -123,6 +136,22 @@ def __exit__(
123136
) -> None:
124137
self.client.__exit__(exc_type, exc_value, traceback)
125138

139+
def info(self) -> ReadTokenInfo:
140+
"""Get information about the read token."""
141+
response = self.client.get('/api/read-token-info')
142+
self.handle_response_errors(response)
143+
token_info = response.json()
144+
try:
145+
# Keep keys defined in ReadTokenInfo
146+
return {
147+
'organization_name': token_info['organization_name'],
148+
'project_name': token_info['project_name'],
149+
}
150+
except KeyError:
151+
raise InfoRequestError(
152+
'The read token info response is missing required fields: organization_name or project_name'
153+
)
154+
126155
def query_json(
127156
self,
128157
sql: str,
@@ -248,6 +277,22 @@ async def __aexit__(
248277
) -> None:
249278
await self.client.__aexit__(exc_type, exc_value, traceback)
250279

280+
async def info(self) -> ReadTokenInfo:
281+
"""Get information about the read token."""
282+
response = await self.client.get('/api/read-token-info')
283+
self.handle_response_errors(response)
284+
token_info = response.json()
285+
# Keep keys defined in ReadTokenInfo
286+
try:
287+
return {
288+
'organization_name': token_info['organization_name'],
289+
'project_name': token_info['project_name'],
290+
}
291+
except KeyError:
292+
raise InfoRequestError(
293+
'The read token info response is missing required fields: organization_name or project_name'
294+
)
295+
251296
async def query_json(
252297
self,
253298
sql: str,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
accept:
6+
- '*/*'
7+
accept-encoding:
8+
- gzip, deflate, zstd
9+
connection:
10+
- keep-alive
11+
host:
12+
- localhost:8000
13+
user-agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: http://localhost:8000/api/read-token-info
17+
response:
18+
body:
19+
string: '{"token_id":"b630bd4e-6a20-486b-95cb-c02a175767b2","organization_id":"710c0de0-dda1-40fb-9601-b72d120acb71","project_id":"11d5d03b-7fc6-4ad6-bcd9-d1cc4bc87986","organization_name":"test-org","project_name":"starter-project"}'
20+
headers:
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '221'
25+
Content-Type:
26+
- application/json
27+
Date:
28+
- Sun, 06 Jul 2025 12:57:40 GMT
29+
Server:
30+
- nginx/1.29.0
31+
access-control-expose-headers:
32+
- traceresponse
33+
traceresponse:
34+
- 00-0197dfd050205c9d29c0a1a4a934222c-03c3392466156875-01
35+
x-api-version:
36+
- UClDZTsUC0iVNKhEvlUL2JNm0ur0dBUm1u2/5He9ZtE=
37+
status:
38+
code: 200
39+
message: OK
40+
version: 1
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
accept:
6+
- '*/*'
7+
accept-encoding:
8+
- gzip, deflate, zstd
9+
connection:
10+
- keep-alive
11+
host:
12+
- localhost:8000
13+
user-agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: http://localhost:8000/api/read-token-info
17+
response:
18+
body:
19+
string: '{"token_id":"b630bd4e-6a20-486b-95cb-c02a175767b2","organization_id":"710c0de0-dda1-40fb-9601-b72d120acb71","project_id":"11d5d03b-7fc6-4ad6-bcd9-d1cc4bc87986","project_name":"starter-project"}'
20+
headers:
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '221'
25+
Content-Type:
26+
- application/json
27+
Date:
28+
- Sun, 06 Jul 2025 13:40:40 GMT
29+
Server:
30+
- nginx/1.29.0
31+
access-control-expose-headers:
32+
- traceresponse
33+
traceresponse:
34+
- 00-0197dff7ac3bae870d3350ef4f961b65-8c6f724ec401043c-01
35+
x-api-version:
36+
- UClDZTsUC0iVNKhEvlUL2JNm0ur0dBUm1u2/5He9ZtE=
37+
status:
38+
code: 200
39+
message: OK
40+
version: 1
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
accept:
6+
- '*/*'
7+
accept-encoding:
8+
- gzip, deflate, zstd
9+
connection:
10+
- keep-alive
11+
host:
12+
- localhost:8000
13+
user-agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: http://localhost:8000/api/read-token-info
17+
response:
18+
body:
19+
string: '{"token_id":"b630bd4e-6a20-486b-95cb-c02a175767b2","organization_id":"710c0de0-dda1-40fb-9601-b72d120acb71","project_id":"11d5d03b-7fc6-4ad6-bcd9-d1cc4bc87986","project_name":"starter-project"}'
20+
headers:
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '221'
25+
Content-Type:
26+
- application/json
27+
Date:
28+
- Sun, 06 Jul 2025 13:44:21 GMT
29+
Server:
30+
- nginx/1.29.0
31+
access-control-expose-headers:
32+
- traceresponse
33+
traceresponse:
34+
- 00-0197dffb0d73874ace548d5d84ed156e-d4a83dfee7bcd915-01
35+
x-api-version:
36+
- UClDZTsUC0iVNKhEvlUL2JNm0ur0dBUm1u2/5He9ZtE=
37+
status:
38+
code: 200
39+
message: OK
40+
version: 1
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
interactions:
2+
- request:
3+
body: ''
4+
headers:
5+
accept:
6+
- '*/*'
7+
accept-encoding:
8+
- gzip, deflate, zstd
9+
connection:
10+
- keep-alive
11+
host:
12+
- localhost:8000
13+
user-agent:
14+
- python-httpx/0.28.1
15+
method: GET
16+
uri: http://localhost:8000/api/read-token-info
17+
response:
18+
body:
19+
string: '{"token_id":"b630bd4e-6a20-486b-95cb-c02a175767b2","organization_id":"710c0de0-dda1-40fb-9601-b72d120acb71","project_id":"11d5d03b-7fc6-4ad6-bcd9-d1cc4bc87986","organization_name":"test-org","project_name":"starter-project"}'
20+
headers:
21+
Connection:
22+
- keep-alive
23+
Content-Length:
24+
- '221'
25+
Content-Type:
26+
- application/json
27+
Date:
28+
- Sun, 06 Jul 2025 12:58:04 GMT
29+
Server:
30+
- nginx/1.29.0
31+
access-control-expose-headers:
32+
- traceresponse
33+
traceresponse:
34+
- 00-0197dfd0ab965a5d8713381b433ff674-77253b890c5a0f74-01
35+
x-api-version:
36+
- UClDZTsUC0iVNKhEvlUL2JNm0ur0dBUm1u2/5He9ZtE=
37+
status:
38+
code: 200
39+
message: OK
40+
version: 1

tests/aaa_query_client/test_query_client.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# To update, set the `CLIENT_BASE_URL` and `CLIENT_READ_TOKEN` values to match the local development environment,
1313
# and run the tests with `--record-mode=rewrite --inline-snapshot=fix` to update the cassettes and snapshots.
1414
CLIENT_BASE_URL = 'http://localhost:8000/'
15-
CLIENT_READ_TOKEN = '06KJCLLch8TCYx1FX4N1VGbr2mHrR760Z87zWjpb0TPm'
15+
CLIENT_READ_TOKEN = 'pylf_v1_local_wk3Vg7NQP1BLtK62PTB0sRqFmn3ThjqvbQn5R27MDZpd'
1616
pytestmark = [
1717
pytest.mark.vcr(),
1818
pytest.mark.skipif(
@@ -39,6 +39,45 @@ def test_infers_base_url_from_token(
3939
assert client.base_url == expected
4040

4141

42+
def test_info_sync():
43+
with LogfireQueryClient(read_token=CLIENT_READ_TOKEN, base_url=CLIENT_BASE_URL) as client:
44+
info = client.info()
45+
assert info == snapshot(
46+
{
47+
'organization_name': 'test-org',
48+
'project_name': 'starter-project',
49+
}
50+
)
51+
52+
53+
def test_info_invalid_schema_sync():
54+
with pytest.raises(
55+
RuntimeError, match='The read token info response is missing required fields: organization_name or project_name'
56+
):
57+
with LogfireQueryClient(read_token=CLIENT_READ_TOKEN, base_url=CLIENT_BASE_URL) as client:
58+
client.info()
59+
60+
61+
@pytest.mark.anyio
62+
async def test_info_async():
63+
async with AsyncLogfireQueryClient(read_token=CLIENT_READ_TOKEN, base_url=CLIENT_BASE_URL) as client:
64+
info = await client.info()
65+
assert info == snapshot(
66+
{
67+
'organization_name': 'test-org',
68+
'project_name': 'starter-project',
69+
}
70+
)
71+
72+
73+
async def test_info_invalid_schema_async():
74+
with pytest.raises(
75+
RuntimeError, match='The read token info response is missing required fields: organization_name or project_name'
76+
):
77+
async with AsyncLogfireQueryClient(read_token=CLIENT_READ_TOKEN, base_url=CLIENT_BASE_URL) as client:
78+
await client.info()
79+
80+
4281
def test_read_sync():
4382
with LogfireQueryClient(read_token=CLIENT_READ_TOKEN, base_url=CLIENT_BASE_URL) as client:
4483
sql = """

0 commit comments

Comments
 (0)