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.
@@ -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]