Skip to content

Commit 6a4b237

Browse files
Enhance client SDK and update documentation
- Added `pydantic` as a dependency for improved data validation in the client SDK. - Introduced `AuraClient`, `ClientLogInputs`, and `auralog` for structured logging in the client SDK. - Updated `README.md` and user documentation to include client SDK usage examples and integration snippets. - Refactored `test_clientlog.py` to utilize the new client logging methods, improving test clarity and functionality. - Enhanced `init.py` with a new client integration snippet for easier setup.
1 parent 93bbd3b commit 6a4b237

10 files changed

Lines changed: 196 additions & 67 deletions

File tree

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,23 @@ Python keeps snake_case naming, but key public names map directly to Node concep
4545
| `AuraServer.log(...)` | `AuraServer.log(...)` and `aura_log(...)` |
4646
| `AuraServer.closeSocket(...)` | `AuraServer.close_socket(...)` and `close_aura_log_socket()` |
4747
| `fetchProjAuthConfig(...)` | `fetch_proj_auth_config(...)` (`fetch_proj_auth_payload(...)` remains supported) |
48-
| `AuraClient` / `clientlog(...)` | Not yet exposed as a Python library API |
48+
| `AuraClient` / `clientlog(...)` | `AuraClient`, `client_log(...)`, and typed `auralog(ClientLogInputs(...))` |
49+
50+
Client SDK quickstart:
51+
52+
```python
53+
from auralogger.client import AuraClient, ClientLogInputs, auralog
54+
55+
AuraClient.sync_from_secret("project-token")
56+
auralog(
57+
ClientLogInputs(
58+
type="info",
59+
message="hello from client sdk",
60+
location="example/client",
61+
data={"source": "python"},
62+
)
63+
)
64+
```
4965

5066
## Commands
5167

auralogger/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
build_proj_auth_url,
77
build_project_logs_url,
88
)
9+
from auralogger.client.client_log import (
10+
AuraClient,
11+
ClientLogInputs,
12+
auralog,
13+
client_log,
14+
close_client_log_socket,
15+
)
916
from auralogger.commands.client_check import run_client_check
1017
from auralogger.commands.init import run_init
1118
from auralogger.commands.server_check import run_server_check
@@ -28,6 +35,11 @@
2835
"aura_log",
2936
"close_aura_log_socket",
3037
"AuraServer",
38+
"AuraClient",
39+
"ClientLogInputs",
40+
"auralog",
41+
"client_log",
42+
"close_client_log_socket",
3143
"log",
3244
"run_get_logs",
3345
"normalize_and_validate_filters",

auralogger/commands/init.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,48 @@ def _build_server_integration_snippet() -> str:
129129
)
130130

131131

132+
def _build_client_integration_snippet() -> str:
133+
return "\n".join(
134+
[
135+
"from typing import Any, Dict, Literal, Optional",
136+
"from pydantic import BaseModel, Field",
137+
"from auralogger.client import AuraClient",
138+
"",
139+
"class ClientLogInputs(BaseModel):",
140+
" type: Literal['debug', 'info', 'warn', 'error'] = 'info'",
141+
" message: str = Field(..., min_length=1)",
142+
" location: Optional[str] = None",
143+
" data: Optional[Dict[str, Any]] = None",
144+
"",
145+
"def configure_client_logger(project_token: str) -> None:",
146+
" AuraClient.sync_from_secret(project_token)",
147+
"",
148+
"def auralog(loginputs: ClientLogInputs) -> None:",
149+
" AuraClient.log(",
150+
" loginputs.type,",
151+
" loginputs.message,",
152+
" loginputs.location,",
153+
" loginputs.data,",
154+
" )",
155+
]
156+
)
157+
158+
132159
def _print_integration_help() -> None:
133160
print()
134161
print("Server integration snippet (Python)")
135162
print()
136163
print(_build_server_integration_snippet())
137164
print()
138165
print(
139-
"Frontend/browser integration is handled by the Node package "
140-
"auralogger-cli/client. Python package support here is server-side."
166+
"Client/browser-ingest integration snippet (Python SDK)."
167+
)
168+
print()
169+
print(_build_client_integration_snippet())
170+
print()
171+
print(
172+
"If your frontend runtime is JavaScript/TypeScript, use the Node package "
173+
"auralogger-cli/client instead."
141174
)
142175
print()
143176

auralogger/commands/test_clientlog.py

Lines changed: 10 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,26 @@
22

33
from __future__ import annotations
44

5-
import json
65
import time
7-
from datetime import datetime, timezone
8-
from typing import Any, Dict, cast
9-
10-
import websocket
11-
from websocket import create_connection
12-
13-
from auralogger.backend_origin import (
14-
build_create_browser_logs_url,
15-
resolve_ws_base_url,
16-
)
6+
from auralogger.client.client_log import AuraClient, close_client_log_socket
177
from auralogger.cli_auth import resolve_project_token_for_init
18-
from auralogger.proj_auth import fetch_proj_auth_payload
19-
20-
CONNECT_TIMEOUT_S = 5
21-
22-
23-
def _iso_timestamp_with_micros(epoch_ms: float) -> str:
24-
dt = datetime.fromtimestamp(epoch_ms / 1000.0, tz=timezone.utc)
25-
base = dt.strftime("%Y-%m-%dT%H:%M:%S")
26-
micros = f"{dt.microsecond:06d}"
27-
return f"{base}.{micros}Z"
288

299

3010
def run_test_clientlog() -> None:
3111
project_token = resolve_project_token_for_init()
32-
raw = fetch_proj_auth_payload(project_token)
33-
auth = cast(Dict[str, Any], raw)
34-
35-
project_id = auth.get("project_id")
36-
project_name = auth.get("project_name")
37-
session_raw = auth.get("session")
38-
session = session_raw.strip() if isinstance(session_raw, str) else ""
39-
if not session:
40-
raise ValueError("proj_auth response did not include a session string.")
41-
42-
ws_base = resolve_ws_base_url()
43-
ws_url = build_create_browser_logs_url(ws_base, project_token)
12+
AuraClient.sync_from_secret(project_token)
4413
print("Sending 5 client test logs via browser ingest route...")
4514

46-
try:
47-
# Browser ingest route is path-auth only; do not send auth headers.
48-
ws = create_connection(ws_url, timeout=CONNECT_TIMEOUT_S)
49-
except websocket.WebSocketTimeoutException as e:
50-
raise ValueError(
51-
f"Browser ingest connect timed out after {CONNECT_TIMEOUT_S * 1000}ms."
52-
) from e
53-
except Exception as e:
54-
raise ValueError(f"auralogger: test-clientlog connect failed: {e}") from e
55-
5615
try:
5716
for i in range(1, 6):
58-
payload = {
59-
"type": "info",
60-
"message": f"test-clientlog log {i}/5",
61-
"location": "cli/test-clientlog",
62-
"session": session,
63-
"created_at": _iso_timestamp_with_micros(time.time() * 1000.0),
64-
"data": json.dumps({"i": i, "kind": "test-clientlog"}),
65-
}
66-
ws.send(json.dumps(payload))
17+
AuraClient.log(
18+
"info",
19+
f"test-clientlog log {i}/5",
20+
"cli/test-clientlog",
21+
{"i": i, "kind": "test-clientlog"},
22+
)
6723
time.sleep(0.15)
68-
except (TypeError, ValueError) as e:
69-
raise ValueError(f"Could not pack test-clientlog payload: {e}") from e
70-
except Exception as e:
71-
raise ValueError(f"Client burst did not send - {e}") from e
7224
finally:
73-
try:
74-
ws.close()
75-
except Exception:
76-
pass
25+
close_client_log_socket()
7726

78-
label = project_name if isinstance(project_name, str) and project_name.strip() else project_id
79-
print(f"Client burst complete for project {label!s}. Try: auralogger get-logs -maxcount 20")
27+
print("Client burst complete. Try: auralogger get-logs -maxcount 20")

dev-docs/file-map.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ Use these pairs when keeping Python terminology close to Node while preserving P
5858
## Namespaced compatibility exports
5959

6060
- `auralogger/server/__init__.py` — server runtime re-exports (`AuraServer`, `aura_log`, `close_aura_log_socket`)
61-
- `auralogger/client/__init__.py` — client-related CLI helper re-exports (`run_client_check`, `run_test_clientlog`)
61+
- `auralogger/client/__init__.py` — client SDK + CLI re-exports (`AuraClient`, `client_log`, typed `auralog`, plus check/test commands)
62+
- `auralogger/client/client_log.py` — importable browser-ingest runtime (`AuraClient`) with token override, `proj_auth` hydration cache, socket reuse + idle close, and typed inputs
6263
- `auralogger/utils/__init__.py` — URL/env/error utility re-exports for stable namespaced imports
6364

6465
## Configuration (os.environ)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ classifiers = [
2727
"Topic :: System :: Logging",
2828
]
2929
dependencies = [
30+
"pydantic>=2.0.0",
3031
"python-dotenv>=1.0.0",
3132
"websocket-client>=1.6.0",
3233
]

tests/test_client_sdk.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
from unittest.mock import patch
5+
6+
from auralogger.client.client_log import (
7+
AuraClient,
8+
ClientLogInputs,
9+
auralog,
10+
client_log,
11+
close_client_log_socket,
12+
)
13+
14+
15+
class _FakeSocket:
16+
def __init__(self) -> None:
17+
self.sent: list[str] = []
18+
self.closed = False
19+
20+
def send(self, body: str) -> None:
21+
self.sent.append(body)
22+
23+
def close(self) -> None:
24+
self.closed = True
25+
26+
27+
class AuraClientSdkTests(unittest.TestCase):
28+
def test_configure_rejects_empty_token(self) -> None:
29+
with self.assertRaises(ValueError):
30+
AuraClient.configure(" ")
31+
32+
def test_sync_from_secret_validates_required_fields(self) -> None:
33+
with patch(
34+
"auralogger.client.client_log.fetch_proj_auth_payload",
35+
return_value={"project_id": "p1", "session": ""},
36+
):
37+
with self.assertRaises(ValueError):
38+
AuraClient.sync_from_secret("cipher-token")
39+
40+
def test_log_delegates_to_client_log_function(self) -> None:
41+
with patch("auralogger.client.client_log.client_log") as mocked:
42+
AuraClient.log("info", "hello", "tests/client", {"k": 1})
43+
mocked.assert_called_once_with("info", "hello", "tests/client", {"k": 1})
44+
45+
def test_auralog_uses_typed_inputs(self) -> None:
46+
with patch("auralogger.client.client_log.AuraClient.log") as mocked:
47+
auralog(
48+
ClientLogInputs(
49+
type="warn",
50+
message="typed input",
51+
location="tests/client",
52+
data={"ok": True},
53+
)
54+
)
55+
mocked.assert_called_once_with("warn", "typed input", "tests/client", {"ok": True})
56+
57+
def test_client_log_sends_when_runtime_is_hydrated(self) -> None:
58+
ws = _FakeSocket()
59+
with patch(
60+
"auralogger.client.client_log._resolve_project_token_runtime",
61+
return_value="ptok",
62+
), patch(
63+
"auralogger.client.client_log._merged_runtime_for_send",
64+
return_value={"project_id": "pid", "session": "sess", "styles": []},
65+
), patch(
66+
"auralogger.client.client_log._ensure_ws",
67+
return_value=ws,
68+
), patch("auralogger.client.client_log.print_log"), patch(
69+
"auralogger.client.client_log._schedule_socket_idle_close"
70+
):
71+
client_log("info", "hello", "tests/client", {"x": 1})
72+
73+
self.assertEqual(len(ws.sent), 1)
74+
75+
def test_close_socket_delegates(self) -> None:
76+
with patch("auralogger.client.client_log.close_client_log_socket") as mocked:
77+
AuraClient.close_socket()
78+
mocked.assert_called_once_with()
79+
80+
def test_close_client_log_socket_closes_ws(self) -> None:
81+
ws = _FakeSocket()
82+
with patch("auralogger.client.client_log._ws", ws), patch(
83+
"auralogger.client.client_log._bound_url", "wss://x"
84+
):
85+
close_client_log_socket()
86+
self.assertTrue(ws.closed)
87+
88+
89+
if __name__ == "__main__":
90+
unittest.main()

tests/test_public_api_names.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
import unittest
44
from unittest.mock import patch
55

6-
from auralogger import AuraServer, fetch_proj_auth_config, fetch_proj_auth_payload
6+
from auralogger import (
7+
AuraClient,
8+
AuraServer,
9+
ClientLogInputs,
10+
auralog,
11+
fetch_proj_auth_config,
12+
fetch_proj_auth_payload,
13+
)
714

815

916
class PublicApiNamingTests(unittest.TestCase):
@@ -37,6 +44,23 @@ def test_sync_from_secret_validates_required_fields(self) -> None:
3744
with self.assertRaises(ValueError):
3845
AuraServer.sync_from_secret("cipher-token")
3946

47+
def test_aura_client_log_delegates_to_client_log(self) -> None:
48+
with patch("auralogger.client.client_log.client_log") as mocked:
49+
AuraClient.log("info", "hello", "tests/public-api", {"k": 1})
50+
mocked.assert_called_once_with("info", "hello", "tests/public-api", {"k": 1})
51+
52+
def test_client_auralog_uses_model_fields(self) -> None:
53+
with patch("auralogger.client.client_log.AuraClient.log") as mocked:
54+
auralog(
55+
ClientLogInputs(
56+
type="info",
57+
message="hello",
58+
location="tests/public-api",
59+
data={"k": 1},
60+
)
61+
)
62+
mocked.assert_called_once_with("info", "hello", "tests/public-api", {"k": 1})
63+
4064

4165
if __name__ == "__main__":
4266
unittest.main()

user-docs/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Default API/WebSocket hosts need no URL config unless you override them. See **[
3030
- `auralogger test-clientlog` — send 5 browser-ingest test logs.
3131
- `auralogger get-logs [filters...]` — fetch and print logs.
3232

33+
For application/runtime usage (not CLI), Python now exposes an importable client SDK:
34+
`from auralogger.client import AuraClient, ClientLogInputs, auralog`.
35+
3336
If you run an unknown command, the CLI exits with code `1`, prints `Unknown command: <name>`, and shows usage plus valid commands so you can retry quickly.
3437

3538
---

user-docs/environment.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Older names such as `AURALOGGER_SECRET_KEY` are **not** read.
3939
| **`auralogger test-clientlog`** | Prompts for missing token, then sends a 5-log burst through `create_browser_logs` using path-only auth. |
4040
| **`auralogger get-logs`** | `AURALOGGER_PROJECT_TOKEN` and `AURALOGGER_USER_SECRET`; styles from env or fetched once via `proj_auth` for that run. |
4141
| **`aura_log()`** | `AURALOGGER_PROJECT_TOKEN` and `AURALOGGER_USER_SECRET`; id/session/styles from env or from a cached `proj_auth` fetch. Otherwise console-only with a one-time stderr hint. |
42+
| **`AuraClient` / `client_log()`** | `AURALOGGER_PROJECT_TOKEN` (or `AuraClient.configure(...)`) and reachable `proj_auth` for full send path; falls back to console-only if project/session hydration fails. |
4243

4344
## Optional
4445

0 commit comments

Comments
 (0)