aepipe-sdk 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.
- aepipe_sdk-0.1.0/PKG-INFO +197 -0
- aepipe_sdk-0.1.0/README.md +180 -0
- aepipe_sdk-0.1.0/aepipe/__init__.py +25 -0
- aepipe_sdk-0.1.0/aepipe/client.py +187 -0
- aepipe_sdk-0.1.0/aepipe/types.py +58 -0
- aepipe_sdk-0.1.0/aepipe_sdk.egg-info/PKG-INFO +197 -0
- aepipe_sdk-0.1.0/aepipe_sdk.egg-info/SOURCES.txt +11 -0
- aepipe_sdk-0.1.0/aepipe_sdk.egg-info/dependency_links.txt +1 -0
- aepipe_sdk-0.1.0/aepipe_sdk.egg-info/top_level.txt +1 -0
- aepipe_sdk-0.1.0/pyproject.toml +27 -0
- aepipe_sdk-0.1.0/setup.cfg +4 -0
- aepipe_sdk-0.1.0/tests/test_client.py +292 -0
- aepipe_sdk-0.1.0/tests/test_integration.py +255 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aepipe-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for aepipe — multi-tenant log ingestion and query
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/loadchange/aepipe
|
|
7
|
+
Project-URL: Repository, https://github.com/loadchange/aepipe
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# aepipe-sdk-python
|
|
19
|
+
|
|
20
|
+
Python SDK for [aepipe](https://github.com/loadchange/aepipe), a multi-tenant log ingestion and query service built on Cloudflare Workers Analytics Engine.
|
|
21
|
+
|
|
22
|
+
Zero external dependencies. Python 3.10+.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install aepipe-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from aepipe import Aepipe
|
|
34
|
+
|
|
35
|
+
client = Aepipe(
|
|
36
|
+
base_url="https://aepipe.yourdomain.com",
|
|
37
|
+
token="your-admin-token",
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API Reference
|
|
42
|
+
|
|
43
|
+
### `ingest(project, logstore, points) -> IngestResult`
|
|
44
|
+
|
|
45
|
+
Write structured event points to Analytics Engine.
|
|
46
|
+
|
|
47
|
+
| Parameter | Type | Description |
|
|
48
|
+
|------------|--------------------|-------------------------------------------------|
|
|
49
|
+
| `project` | `str` | Project name (`^[a-zA-Z0-9_-]{1,64}$`) |
|
|
50
|
+
| `logstore` | `str` | Logstore name (`^[a-zA-Z0-9_-]{1,64}$`) |
|
|
51
|
+
| `points` | `list[DataPoint]` | Event points (max 250 per call) |
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from aepipe import DataPoint
|
|
55
|
+
|
|
56
|
+
result = client.ingest("myapp", "backend", [
|
|
57
|
+
DataPoint(event="user_login"),
|
|
58
|
+
DataPoint(event="api_error", level="error", blobs=["GET /api"], doubles=[1.23]),
|
|
59
|
+
])
|
|
60
|
+
print(result.ok) # True
|
|
61
|
+
print(result.written) # 2
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `log(project, logstore, logs) -> LogResult`
|
|
65
|
+
|
|
66
|
+
Write raw log entries via Workers Observability.
|
|
67
|
+
|
|
68
|
+
| Parameter | Type | Description |
|
|
69
|
+
|------------|-------------------|-------------------------------------------------|
|
|
70
|
+
| `project` | `str` | Project name |
|
|
71
|
+
| `logstore` | `str` | Logstore name |
|
|
72
|
+
| `logs` | `list[LogEntry]` | Log entries (max 250 per call) |
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from aepipe import LogEntry
|
|
76
|
+
|
|
77
|
+
result = client.log("myapp", "backend", [
|
|
78
|
+
LogEntry(message="server started"),
|
|
79
|
+
LogEntry(message="connection timeout", level="error", extra={"ip": "1.2.3.4"}),
|
|
80
|
+
])
|
|
81
|
+
print(result.written) # 2
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `query(project, logstore, sql) -> QueryResult`
|
|
85
|
+
|
|
86
|
+
Run a SQL query against Analytics Engine. Tenant filters are injected automatically.
|
|
87
|
+
|
|
88
|
+
| Parameter | Type | Description |
|
|
89
|
+
|------------|--------|------------------------------------------|
|
|
90
|
+
| `project` | `str` | Project name |
|
|
91
|
+
| `logstore` | `str` | Logstore name |
|
|
92
|
+
| `sql` | `str` | SQL query (Analytics Engine dialect) |
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
result = client.query("myapp", "backend", "SELECT count() as cnt FROM aepipe")
|
|
96
|
+
print(result.data)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `rawlog(project, logstore, *, limit=50, start=None, end=None) -> RawLogResult`
|
|
100
|
+
|
|
101
|
+
Query raw Worker logs via Cloudflare Telemetry API.
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Default | Description |
|
|
104
|
+
|------------|----------------|---------|--------------------------------|
|
|
105
|
+
| `project` | `str` | | Project name |
|
|
106
|
+
| `logstore` | `str` | | Logstore name |
|
|
107
|
+
| `limit` | `int` | `50` | Max results (server caps at 200)|
|
|
108
|
+
| `start` | `str or None` | `None` | Start timestamp (ISO 8601) |
|
|
109
|
+
| `end` | `str or None` | `None` | End timestamp (ISO 8601) |
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = client.rawlog("myapp", "backend", limit=100, start="2025-01-01T00:00:00Z")
|
|
113
|
+
for entry in result.logs:
|
|
114
|
+
print(f"[{entry.level}] {entry.timestamp} {entry.data}")
|
|
115
|
+
print(f"total: {result.count}")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `list_projects() -> ListResult`
|
|
119
|
+
|
|
120
|
+
List all projects that have written data.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
result = client.list_projects()
|
|
124
|
+
print(result.items) # ["myapp", "analytics", ...]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `list_logstores(project) -> ListResult`
|
|
128
|
+
|
|
129
|
+
List all logstores within a project.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
result = client.list_logstores("myapp")
|
|
133
|
+
print(result.items) # ["backend", "frontend", ...]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Data Types
|
|
137
|
+
|
|
138
|
+
### `DataPoint`
|
|
139
|
+
|
|
140
|
+
| Field | Type | Default | Description |
|
|
141
|
+
|-----------|-----------------|----------|---------------------------------|
|
|
142
|
+
| `event` | `str` | required | Event name (non-empty) |
|
|
143
|
+
| `level` | `str` | `"info"` | Log level |
|
|
144
|
+
| `blobs` | `list[str]` | `[]` | String metadata (max 16 extra) |
|
|
145
|
+
| `doubles` | `list[float]` | `[]` | Numeric metrics (max 20) |
|
|
146
|
+
|
|
147
|
+
### `LogEntry`
|
|
148
|
+
|
|
149
|
+
| Field | Type | Default | Description |
|
|
150
|
+
|-----------|-------------------|----------|---------------------------------|
|
|
151
|
+
| `message` | `str` | required | Log message (non-empty) |
|
|
152
|
+
| `level` | `str` | `"info"` | Log level |
|
|
153
|
+
| `extra` | `dict[str, Any]` | `{}` | Additional fields |
|
|
154
|
+
|
|
155
|
+
### Result Types
|
|
156
|
+
|
|
157
|
+
| Type | Fields |
|
|
158
|
+
|-----------------|-------------------------------|
|
|
159
|
+
| `IngestResult` | `ok: bool`, `written: int` |
|
|
160
|
+
| `LogResult` | `ok: bool`, `written: int` |
|
|
161
|
+
| `QueryResult` | `data: Any` |
|
|
162
|
+
| `RawLogResult` | `logs: list[RawLogEntry]`, `count: int` |
|
|
163
|
+
| `RawLogEntry` | `timestamp: str`, `level: str`, `data: Any` |
|
|
164
|
+
| `ListResult` | `items: list[str]` |
|
|
165
|
+
|
|
166
|
+
## Validation
|
|
167
|
+
|
|
168
|
+
All methods validate inputs before making network requests:
|
|
169
|
+
|
|
170
|
+
- **Name format**: project and logstore names must match `^[a-zA-Z0-9_-]{1,64}$`.
|
|
171
|
+
- **Batch size**: `ingest()` and `log()` accept at most 250 items per call.
|
|
172
|
+
|
|
173
|
+
Invalid inputs raise `ValidationError` (subclass of `ValueError`).
|
|
174
|
+
|
|
175
|
+
## Error Handling
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from aepipe import AepipeError, ValidationError
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
client.ingest("myapp", "backend", points)
|
|
182
|
+
except ValidationError as e:
|
|
183
|
+
# Client-side validation failed
|
|
184
|
+
print(f"invalid input: {e}")
|
|
185
|
+
except AepipeError as e:
|
|
186
|
+
# Server returned an error
|
|
187
|
+
print(f"API error {e.status}: {e.message}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Error Class | Parent | Fields | When |
|
|
191
|
+
|------------------|------------|----------------------|----------------------------|
|
|
192
|
+
| `AepipeError` | `Exception`| `status`, `message` | Server returns non-2xx |
|
|
193
|
+
| `ValidationError`| `ValueError`| `message` | Invalid client input |
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# aepipe-sdk-python
|
|
2
|
+
|
|
3
|
+
Python SDK for [aepipe](https://github.com/loadchange/aepipe), a multi-tenant log ingestion and query service built on Cloudflare Workers Analytics Engine.
|
|
4
|
+
|
|
5
|
+
Zero external dependencies. Python 3.10+.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install aepipe-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from aepipe import Aepipe
|
|
17
|
+
|
|
18
|
+
client = Aepipe(
|
|
19
|
+
base_url="https://aepipe.yourdomain.com",
|
|
20
|
+
token="your-admin-token",
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API Reference
|
|
25
|
+
|
|
26
|
+
### `ingest(project, logstore, points) -> IngestResult`
|
|
27
|
+
|
|
28
|
+
Write structured event points to Analytics Engine.
|
|
29
|
+
|
|
30
|
+
| Parameter | Type | Description |
|
|
31
|
+
|------------|--------------------|-------------------------------------------------|
|
|
32
|
+
| `project` | `str` | Project name (`^[a-zA-Z0-9_-]{1,64}$`) |
|
|
33
|
+
| `logstore` | `str` | Logstore name (`^[a-zA-Z0-9_-]{1,64}$`) |
|
|
34
|
+
| `points` | `list[DataPoint]` | Event points (max 250 per call) |
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from aepipe import DataPoint
|
|
38
|
+
|
|
39
|
+
result = client.ingest("myapp", "backend", [
|
|
40
|
+
DataPoint(event="user_login"),
|
|
41
|
+
DataPoint(event="api_error", level="error", blobs=["GET /api"], doubles=[1.23]),
|
|
42
|
+
])
|
|
43
|
+
print(result.ok) # True
|
|
44
|
+
print(result.written) # 2
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `log(project, logstore, logs) -> LogResult`
|
|
48
|
+
|
|
49
|
+
Write raw log entries via Workers Observability.
|
|
50
|
+
|
|
51
|
+
| Parameter | Type | Description |
|
|
52
|
+
|------------|-------------------|-------------------------------------------------|
|
|
53
|
+
| `project` | `str` | Project name |
|
|
54
|
+
| `logstore` | `str` | Logstore name |
|
|
55
|
+
| `logs` | `list[LogEntry]` | Log entries (max 250 per call) |
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from aepipe import LogEntry
|
|
59
|
+
|
|
60
|
+
result = client.log("myapp", "backend", [
|
|
61
|
+
LogEntry(message="server started"),
|
|
62
|
+
LogEntry(message="connection timeout", level="error", extra={"ip": "1.2.3.4"}),
|
|
63
|
+
])
|
|
64
|
+
print(result.written) # 2
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `query(project, logstore, sql) -> QueryResult`
|
|
68
|
+
|
|
69
|
+
Run a SQL query against Analytics Engine. Tenant filters are injected automatically.
|
|
70
|
+
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
|------------|--------|------------------------------------------|
|
|
73
|
+
| `project` | `str` | Project name |
|
|
74
|
+
| `logstore` | `str` | Logstore name |
|
|
75
|
+
| `sql` | `str` | SQL query (Analytics Engine dialect) |
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
result = client.query("myapp", "backend", "SELECT count() as cnt FROM aepipe")
|
|
79
|
+
print(result.data)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `rawlog(project, logstore, *, limit=50, start=None, end=None) -> RawLogResult`
|
|
83
|
+
|
|
84
|
+
Query raw Worker logs via Cloudflare Telemetry API.
|
|
85
|
+
|
|
86
|
+
| Parameter | Type | Default | Description |
|
|
87
|
+
|------------|----------------|---------|--------------------------------|
|
|
88
|
+
| `project` | `str` | | Project name |
|
|
89
|
+
| `logstore` | `str` | | Logstore name |
|
|
90
|
+
| `limit` | `int` | `50` | Max results (server caps at 200)|
|
|
91
|
+
| `start` | `str or None` | `None` | Start timestamp (ISO 8601) |
|
|
92
|
+
| `end` | `str or None` | `None` | End timestamp (ISO 8601) |
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
result = client.rawlog("myapp", "backend", limit=100, start="2025-01-01T00:00:00Z")
|
|
96
|
+
for entry in result.logs:
|
|
97
|
+
print(f"[{entry.level}] {entry.timestamp} {entry.data}")
|
|
98
|
+
print(f"total: {result.count}")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `list_projects() -> ListResult`
|
|
102
|
+
|
|
103
|
+
List all projects that have written data.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
result = client.list_projects()
|
|
107
|
+
print(result.items) # ["myapp", "analytics", ...]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `list_logstores(project) -> ListResult`
|
|
111
|
+
|
|
112
|
+
List all logstores within a project.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = client.list_logstores("myapp")
|
|
116
|
+
print(result.items) # ["backend", "frontend", ...]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Data Types
|
|
120
|
+
|
|
121
|
+
### `DataPoint`
|
|
122
|
+
|
|
123
|
+
| Field | Type | Default | Description |
|
|
124
|
+
|-----------|-----------------|----------|---------------------------------|
|
|
125
|
+
| `event` | `str` | required | Event name (non-empty) |
|
|
126
|
+
| `level` | `str` | `"info"` | Log level |
|
|
127
|
+
| `blobs` | `list[str]` | `[]` | String metadata (max 16 extra) |
|
|
128
|
+
| `doubles` | `list[float]` | `[]` | Numeric metrics (max 20) |
|
|
129
|
+
|
|
130
|
+
### `LogEntry`
|
|
131
|
+
|
|
132
|
+
| Field | Type | Default | Description |
|
|
133
|
+
|-----------|-------------------|----------|---------------------------------|
|
|
134
|
+
| `message` | `str` | required | Log message (non-empty) |
|
|
135
|
+
| `level` | `str` | `"info"` | Log level |
|
|
136
|
+
| `extra` | `dict[str, Any]` | `{}` | Additional fields |
|
|
137
|
+
|
|
138
|
+
### Result Types
|
|
139
|
+
|
|
140
|
+
| Type | Fields |
|
|
141
|
+
|-----------------|-------------------------------|
|
|
142
|
+
| `IngestResult` | `ok: bool`, `written: int` |
|
|
143
|
+
| `LogResult` | `ok: bool`, `written: int` |
|
|
144
|
+
| `QueryResult` | `data: Any` |
|
|
145
|
+
| `RawLogResult` | `logs: list[RawLogEntry]`, `count: int` |
|
|
146
|
+
| `RawLogEntry` | `timestamp: str`, `level: str`, `data: Any` |
|
|
147
|
+
| `ListResult` | `items: list[str]` |
|
|
148
|
+
|
|
149
|
+
## Validation
|
|
150
|
+
|
|
151
|
+
All methods validate inputs before making network requests:
|
|
152
|
+
|
|
153
|
+
- **Name format**: project and logstore names must match `^[a-zA-Z0-9_-]{1,64}$`.
|
|
154
|
+
- **Batch size**: `ingest()` and `log()` accept at most 250 items per call.
|
|
155
|
+
|
|
156
|
+
Invalid inputs raise `ValidationError` (subclass of `ValueError`).
|
|
157
|
+
|
|
158
|
+
## Error Handling
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from aepipe import AepipeError, ValidationError
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
client.ingest("myapp", "backend", points)
|
|
165
|
+
except ValidationError as e:
|
|
166
|
+
# Client-side validation failed
|
|
167
|
+
print(f"invalid input: {e}")
|
|
168
|
+
except AepipeError as e:
|
|
169
|
+
# Server returned an error
|
|
170
|
+
print(f"API error {e.status}: {e.message}")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
| Error Class | Parent | Fields | When |
|
|
174
|
+
|------------------|------------|----------------------|----------------------------|
|
|
175
|
+
| `AepipeError` | `Exception`| `status`, `message` | Server returns non-2xx |
|
|
176
|
+
| `ValidationError`| `ValueError`| `message` | Invalid client input |
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .client import Aepipe, AepipeError, ValidationError
|
|
2
|
+
from .types import (
|
|
3
|
+
DataPoint,
|
|
4
|
+
IngestResult,
|
|
5
|
+
ListResult,
|
|
6
|
+
LogEntry,
|
|
7
|
+
LogResult,
|
|
8
|
+
QueryResult,
|
|
9
|
+
RawLogEntry,
|
|
10
|
+
RawLogResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Aepipe",
|
|
15
|
+
"AepipeError",
|
|
16
|
+
"ValidationError",
|
|
17
|
+
"DataPoint",
|
|
18
|
+
"IngestResult",
|
|
19
|
+
"ListResult",
|
|
20
|
+
"LogEntry",
|
|
21
|
+
"LogResult",
|
|
22
|
+
"QueryResult",
|
|
23
|
+
"RawLogEntry",
|
|
24
|
+
"RawLogResult",
|
|
25
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.request import Request, urlopen
|
|
7
|
+
from urllib.error import HTTPError
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
DataPoint,
|
|
11
|
+
IngestResult,
|
|
12
|
+
ListResult,
|
|
13
|
+
LogEntry,
|
|
14
|
+
LogResult,
|
|
15
|
+
QueryResult,
|
|
16
|
+
RawLogEntry,
|
|
17
|
+
RawLogResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
|
|
21
|
+
_MAX_BATCH = 250
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AepipeError(Exception):
|
|
25
|
+
"""Raised when the aepipe API returns an error."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status: int, message: str):
|
|
28
|
+
self.status = status
|
|
29
|
+
self.message = message
|
|
30
|
+
super().__init__(f"aepipe error {status}: {message}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(ValueError):
|
|
34
|
+
"""Raised when client-side validation fails."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_name(name: str, label: str) -> None:
|
|
38
|
+
if not _NAME_RE.match(name):
|
|
39
|
+
raise ValidationError(f"invalid {label}: {name!r}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _serialize_point(p: DataPoint) -> dict[str, Any]:
|
|
43
|
+
d: dict[str, Any] = {"event": p.event, "level": p.level}
|
|
44
|
+
if p.blobs:
|
|
45
|
+
d["blobs"] = p.blobs
|
|
46
|
+
if p.doubles:
|
|
47
|
+
d["doubles"] = p.doubles
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Aepipe:
|
|
52
|
+
"""Python SDK for the aepipe analytics engine."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, base_url: str, token: str):
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
base_url: The aepipe worker URL, e.g. ``https://aepipe.example.com``.
|
|
58
|
+
token: The ``ADMIN_TOKEN`` secret.
|
|
59
|
+
"""
|
|
60
|
+
self._base = base_url.rstrip("/")
|
|
61
|
+
self._token = token
|
|
62
|
+
|
|
63
|
+
# --- ingest ---
|
|
64
|
+
|
|
65
|
+
def ingest(
|
|
66
|
+
self,
|
|
67
|
+
project: str,
|
|
68
|
+
logstore: str,
|
|
69
|
+
points: list[DataPoint],
|
|
70
|
+
) -> IngestResult:
|
|
71
|
+
"""Write structured event points (max 250 per call)."""
|
|
72
|
+
_validate_name(project, "project")
|
|
73
|
+
_validate_name(logstore, "logstore")
|
|
74
|
+
if len(points) > _MAX_BATCH:
|
|
75
|
+
raise ValidationError(f"max {_MAX_BATCH} points per request, got {len(points)}")
|
|
76
|
+
body = {"points": [_serialize_point(p) for p in points]}
|
|
77
|
+
resp = self._post(f"/v1/{project}/{logstore}/ingest", body)
|
|
78
|
+
return IngestResult(ok=resp["ok"], written=resp["written"])
|
|
79
|
+
|
|
80
|
+
# --- log ---
|
|
81
|
+
|
|
82
|
+
def log(
|
|
83
|
+
self,
|
|
84
|
+
project: str,
|
|
85
|
+
logstore: str,
|
|
86
|
+
logs: list[LogEntry],
|
|
87
|
+
) -> LogResult:
|
|
88
|
+
"""Write raw log entries (max 250 per call)."""
|
|
89
|
+
_validate_name(project, "project")
|
|
90
|
+
_validate_name(logstore, "logstore")
|
|
91
|
+
if len(logs) > _MAX_BATCH:
|
|
92
|
+
raise ValidationError(f"max {_MAX_BATCH} logs per request, got {len(logs)}")
|
|
93
|
+
body = {
|
|
94
|
+
"logs": [
|
|
95
|
+
{"message": e.message, "level": e.level, **e.extra}
|
|
96
|
+
for e in logs
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
resp = self._post(f"/v1/{project}/{logstore}/log", body)
|
|
100
|
+
return LogResult(ok=resp["ok"], written=resp["written"])
|
|
101
|
+
|
|
102
|
+
# --- query ---
|
|
103
|
+
|
|
104
|
+
def query(
|
|
105
|
+
self,
|
|
106
|
+
project: str,
|
|
107
|
+
logstore: str,
|
|
108
|
+
sql: str,
|
|
109
|
+
) -> QueryResult:
|
|
110
|
+
"""Run a SQL query against the Analytics Engine."""
|
|
111
|
+
_validate_name(project, "project")
|
|
112
|
+
_validate_name(logstore, "logstore")
|
|
113
|
+
resp = self._post(f"/v1/{project}/{logstore}/query", {"sql": sql})
|
|
114
|
+
return QueryResult(data=resp)
|
|
115
|
+
|
|
116
|
+
# --- rawlog ---
|
|
117
|
+
|
|
118
|
+
def rawlog(
|
|
119
|
+
self,
|
|
120
|
+
project: str,
|
|
121
|
+
logstore: str,
|
|
122
|
+
*,
|
|
123
|
+
limit: int = 50,
|
|
124
|
+
start: str | None = None,
|
|
125
|
+
end: str | None = None,
|
|
126
|
+
) -> RawLogResult:
|
|
127
|
+
"""Query raw Worker logs."""
|
|
128
|
+
_validate_name(project, "project")
|
|
129
|
+
_validate_name(logstore, "logstore")
|
|
130
|
+
body: dict[str, Any] = {"limit": limit}
|
|
131
|
+
if start is not None:
|
|
132
|
+
body["start"] = start
|
|
133
|
+
if end is not None:
|
|
134
|
+
body["end"] = end
|
|
135
|
+
resp = self._post(f"/v1/{project}/{logstore}/rawlog", body)
|
|
136
|
+
entries = [
|
|
137
|
+
RawLogEntry(
|
|
138
|
+
timestamp=e["timestamp"],
|
|
139
|
+
level=e["level"],
|
|
140
|
+
data=e["data"],
|
|
141
|
+
)
|
|
142
|
+
for e in resp.get("logs", [])
|
|
143
|
+
]
|
|
144
|
+
return RawLogResult(logs=entries, count=resp.get("count", len(entries)))
|
|
145
|
+
|
|
146
|
+
# --- list ---
|
|
147
|
+
|
|
148
|
+
def list_projects(self) -> ListResult:
|
|
149
|
+
"""List all projects."""
|
|
150
|
+
resp = self._get("/v1/projects")
|
|
151
|
+
return ListResult(items=resp.get("projects", []))
|
|
152
|
+
|
|
153
|
+
def list_logstores(self, project: str) -> ListResult:
|
|
154
|
+
"""List all logstores in a project."""
|
|
155
|
+
_validate_name(project, "project")
|
|
156
|
+
resp = self._get(f"/v1/{project}/logstores")
|
|
157
|
+
return ListResult(items=resp.get("logstores", []))
|
|
158
|
+
|
|
159
|
+
# --- internal ---
|
|
160
|
+
|
|
161
|
+
def _headers(self) -> dict[str, str]:
|
|
162
|
+
return {
|
|
163
|
+
"Authorization": f"Bearer {self._token}",
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"User-Agent": "aepipe-sdk-python/0.1.0",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def _request(self, method: str, path: str, body: Any = None) -> Any:
|
|
169
|
+
url = f"{self._base}{path}"
|
|
170
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
171
|
+
req = Request(url, data=data, headers=self._headers(), method=method)
|
|
172
|
+
try:
|
|
173
|
+
with urlopen(req) as resp:
|
|
174
|
+
return json.loads(resp.read())
|
|
175
|
+
except HTTPError as e:
|
|
176
|
+
text = e.read().decode()
|
|
177
|
+
try:
|
|
178
|
+
msg = json.loads(text).get("error", text)
|
|
179
|
+
except json.JSONDecodeError:
|
|
180
|
+
msg = text
|
|
181
|
+
raise AepipeError(e.code, msg) from e
|
|
182
|
+
|
|
183
|
+
def _get(self, path: str) -> Any:
|
|
184
|
+
return self._request("GET", path)
|
|
185
|
+
|
|
186
|
+
def _post(self, path: str, body: Any) -> Any:
|
|
187
|
+
return self._request("POST", path, body)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class DataPoint:
|
|
9
|
+
"""A structured event point to write to Analytics Engine."""
|
|
10
|
+
|
|
11
|
+
event: str
|
|
12
|
+
level: str = "info"
|
|
13
|
+
blobs: list[str] = field(default_factory=list)
|
|
14
|
+
doubles: list[float] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class LogEntry:
|
|
19
|
+
"""A raw log entry to write via Workers Logs."""
|
|
20
|
+
|
|
21
|
+
message: str
|
|
22
|
+
level: str = "info"
|
|
23
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class IngestResult:
|
|
28
|
+
ok: bool
|
|
29
|
+
written: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class LogResult:
|
|
34
|
+
ok: bool
|
|
35
|
+
written: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class RawLogEntry:
|
|
40
|
+
timestamp: str
|
|
41
|
+
level: str
|
|
42
|
+
data: Any
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class RawLogResult:
|
|
47
|
+
logs: list[RawLogEntry]
|
|
48
|
+
count: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class QueryResult:
|
|
53
|
+
data: Any
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ListResult:
|
|
58
|
+
items: list[str]
|