wirelog 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wirelog-0.1.0/.github/workflows/ci.yml +24 -0
- wirelog-0.1.0/.github/workflows/publish.yml +21 -0
- wirelog-0.1.0/.gitignore +36 -0
- wirelog-0.1.0/LICENSE +21 -0
- wirelog-0.1.0/PKG-INFO +89 -0
- wirelog-0.1.0/README.md +65 -0
- wirelog-0.1.0/pyproject.toml +34 -0
- wirelog-0.1.0/src/wirelog/__init__.py +6 -0
- wirelog-0.1.0/src/wirelog/client.py +138 -0
- wirelog-0.1.0/tests/__init__.py +0 -0
- wirelog-0.1.0/tests/test_client.py +143 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
strategy:
|
|
8
|
+
matrix:
|
|
9
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: ${{ matrix.python-version }}
|
|
15
|
+
- name: Install package
|
|
16
|
+
run: pip install -e .
|
|
17
|
+
- name: Run tests
|
|
18
|
+
run: python -m unittest discover tests -v
|
|
19
|
+
- name: Verify zero dependencies
|
|
20
|
+
run: |
|
|
21
|
+
pip show wirelog | grep -E "^Requires:" | grep -qE "^Requires:\s*$" || \
|
|
22
|
+
(echo "ERROR: wirelog has dependencies!" && exit 1)
|
|
23
|
+
- name: Compile check
|
|
24
|
+
run: python -m py_compile src/wirelog/client.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags: ["v*"]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
publish:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Install build tools
|
|
17
|
+
run: pip install build
|
|
18
|
+
- name: Build package
|
|
19
|
+
run: python -m build
|
|
20
|
+
- name: Publish to PyPI
|
|
21
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
wirelog-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Build/dist
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
|
|
9
|
+
# Virtual env
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# OS
|
|
15
|
+
.DS_Store
|
|
16
|
+
Thumbs.db
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
*.swo
|
|
23
|
+
*~
|
|
24
|
+
|
|
25
|
+
# Env
|
|
26
|
+
.env
|
|
27
|
+
.env.local
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
htmlcov/
|
|
33
|
+
|
|
34
|
+
# Misc
|
|
35
|
+
*.bak
|
|
36
|
+
*.tmp
|
wirelog-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WireLog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
wirelog-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wirelog
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WireLog analytics client — zero dependencies
|
|
5
|
+
Project-URL: Homepage, https://wirelog.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/wirelogai/wirelog-python
|
|
7
|
+
Project-URL: Documentation, https://docs.wirelog.ai
|
|
8
|
+
Author-email: WireLog <hello@wirelog.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,analytics,events,tracking,wirelog
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# wirelog
|
|
26
|
+
|
|
27
|
+
[WireLog](https://wirelog.ai) analytics client for Python. **Zero dependencies** — stdlib only.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install wirelog
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from wirelog import WireLog
|
|
39
|
+
|
|
40
|
+
wl = WireLog(api_key="sk_your_secret_key")
|
|
41
|
+
|
|
42
|
+
# Track an event
|
|
43
|
+
wl.track("signup", user_id="u_123", event_properties={"plan": "free"})
|
|
44
|
+
|
|
45
|
+
# Query analytics (returns Markdown by default)
|
|
46
|
+
result = wl.query("signup | last 7d | count by day")
|
|
47
|
+
print(result)
|
|
48
|
+
|
|
49
|
+
# Identify a user (bind device → user, set profile)
|
|
50
|
+
wl.identify("alice@acme.org", device_id="dev_abc", user_properties={"plan": "pro"})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
wl = WireLog(
|
|
57
|
+
api_key="sk_...", # or set WIRELOG_API_KEY env var
|
|
58
|
+
host="https://api.wirelog.ai", # or set WIRELOG_HOST env var
|
|
59
|
+
timeout=30, # HTTP timeout in seconds
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
### `wl.track(event_type, *, user_id, device_id, session_id, event_properties, user_properties, insert_id)`
|
|
66
|
+
|
|
67
|
+
Track a single event. Auto-generates `insert_id` and `time` if not provided.
|
|
68
|
+
|
|
69
|
+
### `wl.track_batch(events)`
|
|
70
|
+
|
|
71
|
+
Track multiple events in one request (up to 2000).
|
|
72
|
+
|
|
73
|
+
### `wl.query(q, *, format="llm", limit=100, offset=0)`
|
|
74
|
+
|
|
75
|
+
Run a pipe DSL query. Format: `"llm"` (Markdown), `"json"`, or `"csv"`.
|
|
76
|
+
|
|
77
|
+
### `wl.identify(user_id, *, device_id, user_properties, user_property_ops)`
|
|
78
|
+
|
|
79
|
+
Bind a device to a user and/or update profile properties.
|
|
80
|
+
|
|
81
|
+
## Zero Dependencies
|
|
82
|
+
|
|
83
|
+
This library uses only the Python standard library (`urllib.request`, `json`, `time`, `uuid`, `os`). No `requests`, no `httpx`, no `urllib3`. It works out of the box on any Python 3.9+ installation.
|
|
84
|
+
|
|
85
|
+
## Learn More
|
|
86
|
+
|
|
87
|
+
- [WireLog](https://wirelog.ai) — headless analytics for agents and LLMs
|
|
88
|
+
- [Query language docs](https://docs.wirelog.ai/query-language)
|
|
89
|
+
- [API reference](https://docs.wirelog.ai/reference/api)
|
wirelog-0.1.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# wirelog
|
|
2
|
+
|
|
3
|
+
[WireLog](https://wirelog.ai) analytics client for Python. **Zero dependencies** — stdlib only.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install wirelog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from wirelog import WireLog
|
|
15
|
+
|
|
16
|
+
wl = WireLog(api_key="sk_your_secret_key")
|
|
17
|
+
|
|
18
|
+
# Track an event
|
|
19
|
+
wl.track("signup", user_id="u_123", event_properties={"plan": "free"})
|
|
20
|
+
|
|
21
|
+
# Query analytics (returns Markdown by default)
|
|
22
|
+
result = wl.query("signup | last 7d | count by day")
|
|
23
|
+
print(result)
|
|
24
|
+
|
|
25
|
+
# Identify a user (bind device → user, set profile)
|
|
26
|
+
wl.identify("alice@acme.org", device_id="dev_abc", user_properties={"plan": "pro"})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
wl = WireLog(
|
|
33
|
+
api_key="sk_...", # or set WIRELOG_API_KEY env var
|
|
34
|
+
host="https://api.wirelog.ai", # or set WIRELOG_HOST env var
|
|
35
|
+
timeout=30, # HTTP timeout in seconds
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `wl.track(event_type, *, user_id, device_id, session_id, event_properties, user_properties, insert_id)`
|
|
42
|
+
|
|
43
|
+
Track a single event. Auto-generates `insert_id` and `time` if not provided.
|
|
44
|
+
|
|
45
|
+
### `wl.track_batch(events)`
|
|
46
|
+
|
|
47
|
+
Track multiple events in one request (up to 2000).
|
|
48
|
+
|
|
49
|
+
### `wl.query(q, *, format="llm", limit=100, offset=0)`
|
|
50
|
+
|
|
51
|
+
Run a pipe DSL query. Format: `"llm"` (Markdown), `"json"`, or `"csv"`.
|
|
52
|
+
|
|
53
|
+
### `wl.identify(user_id, *, device_id, user_properties, user_property_ops)`
|
|
54
|
+
|
|
55
|
+
Bind a device to a user and/or update profile properties.
|
|
56
|
+
|
|
57
|
+
## Zero Dependencies
|
|
58
|
+
|
|
59
|
+
This library uses only the Python standard library (`urllib.request`, `json`, `time`, `uuid`, `os`). No `requests`, no `httpx`, no `urllib3`. It works out of the box on any Python 3.9+ installation.
|
|
60
|
+
|
|
61
|
+
## Learn More
|
|
62
|
+
|
|
63
|
+
- [WireLog](https://wirelog.ai) — headless analytics for agents and LLMs
|
|
64
|
+
- [Query language docs](https://docs.wirelog.ai/query-language)
|
|
65
|
+
- [API reference](https://docs.wirelog.ai/reference/api)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wirelog"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "WireLog analytics client — zero dependencies"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [{name = "WireLog", email = "hello@wirelog.ai"}]
|
|
9
|
+
keywords = ["analytics", "wirelog", "events", "tracking", "ai-agents"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://wirelog.ai"
|
|
26
|
+
Repository = "https://github.com/wirelogai/wirelog-python"
|
|
27
|
+
Documentation = "https://docs.wirelog.ai"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/wirelog"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""WireLog analytics client. Zero external dependencies — stdlib only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.error import HTTPError
|
|
11
|
+
from urllib.request import Request, urlopen
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WireLogError(Exception):
|
|
15
|
+
"""Raised when the WireLog API returns an error."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status: int, message: str) -> None:
|
|
18
|
+
super().__init__(f"WireLog API {status}: {message}")
|
|
19
|
+
self.status = status
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WireLog:
|
|
23
|
+
"""WireLog analytics client.
|
|
24
|
+
|
|
25
|
+
Zero external dependencies. Uses only the Python standard library.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_key: API key (pk_, sk_, or aat_). Falls back to WIRELOG_API_KEY env var.
|
|
29
|
+
host: API base URL. Falls back to WIRELOG_HOST env var or https://api.wirelog.ai.
|
|
30
|
+
timeout: HTTP timeout in seconds. Default 30.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
host: str | None = None,
|
|
37
|
+
timeout: int = 30,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.api_key = api_key or os.environ.get("WIRELOG_API_KEY", "")
|
|
40
|
+
self.host = (
|
|
41
|
+
host or os.environ.get("WIRELOG_HOST", "https://api.wirelog.ai")
|
|
42
|
+
).rstrip("/")
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
|
|
45
|
+
def track(
|
|
46
|
+
self,
|
|
47
|
+
event_type: str,
|
|
48
|
+
*,
|
|
49
|
+
user_id: str | None = None,
|
|
50
|
+
device_id: str | None = None,
|
|
51
|
+
session_id: str | None = None,
|
|
52
|
+
event_properties: dict[str, Any] | None = None,
|
|
53
|
+
user_properties: dict[str, Any] | None = None,
|
|
54
|
+
insert_id: str | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Track a single event. Returns {"accepted": N}."""
|
|
57
|
+
body: dict[str, Any] = {"event_type": event_type}
|
|
58
|
+
if user_id is not None:
|
|
59
|
+
body["user_id"] = user_id
|
|
60
|
+
if device_id is not None:
|
|
61
|
+
body["device_id"] = device_id
|
|
62
|
+
if session_id is not None:
|
|
63
|
+
body["session_id"] = session_id
|
|
64
|
+
if event_properties is not None:
|
|
65
|
+
body["event_properties"] = event_properties
|
|
66
|
+
if user_properties is not None:
|
|
67
|
+
body["user_properties"] = user_properties
|
|
68
|
+
if insert_id is not None:
|
|
69
|
+
body["insert_id"] = insert_id
|
|
70
|
+
else:
|
|
71
|
+
body["insert_id"] = uuid.uuid4().hex
|
|
72
|
+
body["time"] = _iso_now()
|
|
73
|
+
return self._post("/track", body)
|
|
74
|
+
|
|
75
|
+
def track_batch(self, events: list[dict[str, Any]]) -> dict[str, Any]:
|
|
76
|
+
"""Track multiple events. Returns {"accepted": N}."""
|
|
77
|
+
return self._post("/track", {"events": events})
|
|
78
|
+
|
|
79
|
+
def query(
|
|
80
|
+
self,
|
|
81
|
+
q: str,
|
|
82
|
+
*,
|
|
83
|
+
format: str = "llm",
|
|
84
|
+
limit: int = 100,
|
|
85
|
+
offset: int = 0,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Run a pipe DSL query. Returns Markdown (default), JSON, or CSV."""
|
|
88
|
+
return self._post(
|
|
89
|
+
"/query",
|
|
90
|
+
{"q": q, "format": format, "limit": limit, "offset": offset},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def identify(
|
|
94
|
+
self,
|
|
95
|
+
user_id: str,
|
|
96
|
+
*,
|
|
97
|
+
device_id: str | None = None,
|
|
98
|
+
user_properties: dict[str, Any] | None = None,
|
|
99
|
+
user_property_ops: dict[str, Any] | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Bind device to user and/or set profile properties."""
|
|
102
|
+
body: dict[str, Any] = {"user_id": user_id}
|
|
103
|
+
if device_id is not None:
|
|
104
|
+
body["device_id"] = device_id
|
|
105
|
+
if user_properties is not None:
|
|
106
|
+
body["user_properties"] = user_properties
|
|
107
|
+
if user_property_ops is not None:
|
|
108
|
+
body["user_property_ops"] = user_property_ops
|
|
109
|
+
return self._post("/identify", body)
|
|
110
|
+
|
|
111
|
+
def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
112
|
+
"""Send a POST request to the WireLog API."""
|
|
113
|
+
url = f"{self.host}{path}"
|
|
114
|
+
data = json.dumps(body).encode("utf-8")
|
|
115
|
+
req = Request(
|
|
116
|
+
url,
|
|
117
|
+
data=data,
|
|
118
|
+
headers={
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"X-API-Key": self.api_key,
|
|
121
|
+
},
|
|
122
|
+
method="POST",
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
with urlopen(req, timeout=self.timeout) as resp:
|
|
126
|
+
raw = resp.read()
|
|
127
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
128
|
+
if "application/json" in content_type:
|
|
129
|
+
return json.loads(raw)
|
|
130
|
+
return raw.decode("utf-8")
|
|
131
|
+
except HTTPError as e:
|
|
132
|
+
msg = e.read().decode("utf-8", errors="replace")
|
|
133
|
+
raise WireLogError(e.code, msg) from e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _iso_now() -> str:
|
|
137
|
+
"""Current UTC time in ISO 8601 format."""
|
|
138
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
File without changes
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Unit tests for the WireLog client. Uses only stdlib (no pytest)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import unittest
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
8
|
+
from threading import Thread
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from wirelog import WireLog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MockHandler(BaseHTTPRequestHandler):
|
|
15
|
+
"""Simple mock WireLog API server."""
|
|
16
|
+
|
|
17
|
+
last_request: dict[str, Any] = {}
|
|
18
|
+
response_body: dict[str, Any] = {"accepted": 1}
|
|
19
|
+
response_status: int = 200
|
|
20
|
+
|
|
21
|
+
def do_POST(self) -> None:
|
|
22
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
23
|
+
body = self.rfile.read(length)
|
|
24
|
+
MockHandler.last_request = {
|
|
25
|
+
"path": self.path,
|
|
26
|
+
"headers": dict(self.headers),
|
|
27
|
+
"body": json.loads(body) if body else {},
|
|
28
|
+
}
|
|
29
|
+
self.send_response(MockHandler.response_status)
|
|
30
|
+
self.send_header("Content-Type", "application/json")
|
|
31
|
+
self.end_headers()
|
|
32
|
+
self.wfile.write(json.dumps(MockHandler.response_body).encode())
|
|
33
|
+
|
|
34
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
35
|
+
pass # Suppress request logging
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestWireLogClient(unittest.TestCase):
|
|
39
|
+
server: HTTPServer
|
|
40
|
+
thread: Thread
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def setUpClass(cls) -> None:
|
|
44
|
+
cls.server = HTTPServer(("127.0.0.1", 0), MockHandler)
|
|
45
|
+
cls.thread = Thread(target=cls.server.serve_forever, daemon=True)
|
|
46
|
+
cls.thread.start()
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def tearDownClass(cls) -> None:
|
|
50
|
+
cls.server.shutdown()
|
|
51
|
+
|
|
52
|
+
def _client(self) -> WireLog:
|
|
53
|
+
port = self.server.server_address[1]
|
|
54
|
+
return WireLog(api_key="sk_test_key", host=f"http://127.0.0.1:{port}")
|
|
55
|
+
|
|
56
|
+
def test_track_sends_event(self) -> None:
|
|
57
|
+
MockHandler.response_body = {"accepted": 1}
|
|
58
|
+
MockHandler.response_status = 200
|
|
59
|
+
client = self._client()
|
|
60
|
+
|
|
61
|
+
result = client.track("signup", user_id="u_123", event_properties={"plan": "free"})
|
|
62
|
+
|
|
63
|
+
self.assertEqual(result, {"accepted": 1})
|
|
64
|
+
self.assertEqual(MockHandler.last_request["path"], "/track")
|
|
65
|
+
self.assertEqual(MockHandler.last_request["body"]["event_type"], "signup")
|
|
66
|
+
self.assertEqual(MockHandler.last_request["body"]["user_id"], "u_123")
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
MockHandler.last_request["body"]["event_properties"], {"plan": "free"}
|
|
69
|
+
)
|
|
70
|
+
self.assertIn("insert_id", MockHandler.last_request["body"])
|
|
71
|
+
self.assertIn("time", MockHandler.last_request["body"])
|
|
72
|
+
|
|
73
|
+
def test_track_batch(self) -> None:
|
|
74
|
+
MockHandler.response_body = {"accepted": 2}
|
|
75
|
+
MockHandler.response_status = 200
|
|
76
|
+
client = self._client()
|
|
77
|
+
|
|
78
|
+
events = [
|
|
79
|
+
{"event_type": "page_view", "user_id": "u_1"},
|
|
80
|
+
{"event_type": "click", "user_id": "u_2"},
|
|
81
|
+
]
|
|
82
|
+
result = client.track_batch(events)
|
|
83
|
+
|
|
84
|
+
self.assertEqual(result, {"accepted": 2})
|
|
85
|
+
self.assertEqual(MockHandler.last_request["body"]["events"], events)
|
|
86
|
+
|
|
87
|
+
def test_query(self) -> None:
|
|
88
|
+
MockHandler.response_body = {"rows": [{"count": 42}]}
|
|
89
|
+
MockHandler.response_status = 200
|
|
90
|
+
client = self._client()
|
|
91
|
+
|
|
92
|
+
result = client.query("* | last 7d | count")
|
|
93
|
+
|
|
94
|
+
self.assertEqual(result, {"rows": [{"count": 42}]})
|
|
95
|
+
self.assertEqual(MockHandler.last_request["path"], "/query")
|
|
96
|
+
self.assertEqual(
|
|
97
|
+
MockHandler.last_request["body"]["q"], "* | last 7d | count"
|
|
98
|
+
)
|
|
99
|
+
self.assertEqual(MockHandler.last_request["body"]["format"], "llm")
|
|
100
|
+
|
|
101
|
+
def test_identify(self) -> None:
|
|
102
|
+
MockHandler.response_body = {"ok": True}
|
|
103
|
+
MockHandler.response_status = 200
|
|
104
|
+
client = self._client()
|
|
105
|
+
|
|
106
|
+
result = client.identify(
|
|
107
|
+
"alice@acme.org",
|
|
108
|
+
device_id="dev_123",
|
|
109
|
+
user_properties={"email": "alice@acme.org", "plan": "pro"},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.assertEqual(result, {"ok": True})
|
|
113
|
+
self.assertEqual(MockHandler.last_request["path"], "/identify")
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
MockHandler.last_request["body"]["user_id"], "alice@acme.org"
|
|
116
|
+
)
|
|
117
|
+
self.assertEqual(
|
|
118
|
+
MockHandler.last_request["body"]["device_id"], "dev_123"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def test_api_key_header(self) -> None:
|
|
122
|
+
MockHandler.response_body = {"accepted": 1}
|
|
123
|
+
MockHandler.response_status = 200
|
|
124
|
+
client = self._client()
|
|
125
|
+
|
|
126
|
+
client.track("test")
|
|
127
|
+
|
|
128
|
+
self.assertEqual(
|
|
129
|
+
MockHandler.last_request["headers"]["X-Api-Key"], "sk_test_key"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def test_constructor_defaults(self) -> None:
|
|
133
|
+
client = WireLog()
|
|
134
|
+
self.assertEqual(client.host, "https://api.wirelog.ai")
|
|
135
|
+
self.assertEqual(client.api_key, "")
|
|
136
|
+
|
|
137
|
+
def test_host_trailing_slash_stripped(self) -> None:
|
|
138
|
+
client = WireLog(api_key="sk_test", host="https://api.wirelog.ai/")
|
|
139
|
+
self.assertEqual(client.host, "https://api.wirelog.ai")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
unittest.main()
|