gork-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.
- gork_sdk-0.1.0/PKG-INFO +67 -0
- gork_sdk-0.1.0/README.md +49 -0
- gork_sdk-0.1.0/pyproject.toml +28 -0
- gork_sdk-0.1.0/setup.cfg +4 -0
- gork_sdk-0.1.0/src/gork/__init__.py +30 -0
- gork_sdk-0.1.0/src/gork/client.py +468 -0
- gork_sdk-0.1.0/src/gork/errors.py +9 -0
- gork_sdk-0.1.0/src/gork/types.py +59 -0
- gork_sdk-0.1.0/src/gork_sdk.egg-info/PKG-INFO +67 -0
- gork_sdk-0.1.0/src/gork_sdk.egg-info/SOURCES.txt +11 -0
- gork_sdk-0.1.0/src/gork_sdk.egg-info/dependency_links.txt +1 -0
- gork_sdk-0.1.0/src/gork_sdk.egg-info/top_level.txt +1 -0
- gork_sdk-0.1.0/tests/test_client_requests.py +110 -0
gork_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gork-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for gork.email programmable email infrastructure for AI agents
|
|
5
|
+
Author: gork.email
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gork.email
|
|
8
|
+
Project-URL: Documentation, https://gork.email/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/shyamcxy/gork-sdk-python
|
|
10
|
+
Project-URL: Issues, https://github.com/shyamcxy/gork-sdk-python/issues
|
|
11
|
+
Keywords: email,agent,ai,mcp,inbox,mail
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# gork-sdk — Python SDK for gork.email
|
|
20
|
+
|
|
21
|
+
> **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
|
|
22
|
+
|
|
23
|
+
Programmable email infrastructure for AI agents. Provision inboxes, send and
|
|
24
|
+
receive mail, search a mailbox, compose drafts, schedule sends, read
|
|
25
|
+
attachments, and manage threads, webhooks, domains, keys, and suppressions.
|
|
26
|
+
|
|
27
|
+
This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
|
|
28
|
+
SDK (`@gork/sdk`); the method names and parameters match.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install gork-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from gork import GorkClient
|
|
40
|
+
|
|
41
|
+
gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
|
|
42
|
+
|
|
43
|
+
inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
|
|
44
|
+
print(inbox["address"]) # sdr-alex@try.gork.email
|
|
45
|
+
|
|
46
|
+
gork.send_message(
|
|
47
|
+
inbox_id=inbox["id"],
|
|
48
|
+
to=["prospect@acme.com"],
|
|
49
|
+
subject="Quick intro",
|
|
50
|
+
text="Hi — wanted to share what we're building.",
|
|
51
|
+
idempotency_key="outreach-001", # safe retries
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Webhook verification
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from gork import verify_signature
|
|
59
|
+
|
|
60
|
+
ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Links
|
|
64
|
+
|
|
65
|
+
- Docs: https://gork.email/docs
|
|
66
|
+
- API reference: https://api.gork.email/openapi.json
|
|
67
|
+
- Changelog: https://gork.email/changelog
|
gork_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# gork-sdk — Python SDK for gork.email
|
|
2
|
+
|
|
3
|
+
> **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
|
|
4
|
+
|
|
5
|
+
Programmable email infrastructure for AI agents. Provision inboxes, send and
|
|
6
|
+
receive mail, search a mailbox, compose drafts, schedule sends, read
|
|
7
|
+
attachments, and manage threads, webhooks, domains, keys, and suppressions.
|
|
8
|
+
|
|
9
|
+
This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
|
|
10
|
+
SDK (`@gork/sdk`); the method names and parameters match.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install gork-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from gork import GorkClient
|
|
22
|
+
|
|
23
|
+
gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
|
|
24
|
+
|
|
25
|
+
inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
|
|
26
|
+
print(inbox["address"]) # sdr-alex@try.gork.email
|
|
27
|
+
|
|
28
|
+
gork.send_message(
|
|
29
|
+
inbox_id=inbox["id"],
|
|
30
|
+
to=["prospect@acme.com"],
|
|
31
|
+
subject="Quick intro",
|
|
32
|
+
text="Hi — wanted to share what we're building.",
|
|
33
|
+
idempotency_key="outreach-001", # safe retries
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Webhook verification
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from gork import verify_signature
|
|
41
|
+
|
|
42
|
+
ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Links
|
|
46
|
+
|
|
47
|
+
- Docs: https://gork.email/docs
|
|
48
|
+
- API reference: https://api.gork.email/openapi.json
|
|
49
|
+
- Changelog: https://gork.email/changelog
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gork-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for gork.email programmable email infrastructure for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "gork.email" }]
|
|
13
|
+
keywords = ["email", "agent", "ai", "mcp", "inbox", "mail"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Topic :: Communications :: Email",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://gork.email"
|
|
23
|
+
Documentation = "https://gork.email/docs"
|
|
24
|
+
Repository = "https://github.com/shyamcxy/gork-sdk-python"
|
|
25
|
+
Issues = "https://github.com/shyamcxy/gork-sdk-python/issues"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
gork_sdk-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""gork.email Python SDK — programmable email infrastructure for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .client import GorkClient
|
|
4
|
+
from .errors import GorkError
|
|
5
|
+
from .types import (
|
|
6
|
+
Inbox,
|
|
7
|
+
Message,
|
|
8
|
+
Thread,
|
|
9
|
+
Webhook,
|
|
10
|
+
ApiKey,
|
|
11
|
+
Domain,
|
|
12
|
+
Suppression,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Gork = GorkClient
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Gork",
|
|
19
|
+
"GorkClient",
|
|
20
|
+
"GorkError",
|
|
21
|
+
"Inbox",
|
|
22
|
+
"Message",
|
|
23
|
+
"Thread",
|
|
24
|
+
"Webhook",
|
|
25
|
+
"ApiKey",
|
|
26
|
+
"Domain",
|
|
27
|
+
"Suppression",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""GorkClient — zero-dependency (stdlib only) REST client for api.gork.email."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import time
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .errors import GorkError
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://api.gork.email"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def verify_signature(
|
|
18
|
+
payload: str, signature_header: str, secret: str, tolerance_seconds: int = 300
|
|
19
|
+
) -> bool:
|
|
20
|
+
"""Verify an X-Gork-Signature webhook header (HMAC-SHA256).
|
|
21
|
+
|
|
22
|
+
Rejects signatures older than ``tolerance_seconds`` (default 300s,
|
|
23
|
+
matching the TypeScript verifiers) to prevent replay attacks.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
|
|
27
|
+
timestamp, signature = parts.get("t", ""), parts.get("v1", "")
|
|
28
|
+
if not timestamp or not signature:
|
|
29
|
+
return False
|
|
30
|
+
# Freshness check — a captured payload+signature must not verify
|
|
31
|
+
# forever (previously there was no tolerance window at all).
|
|
32
|
+
if abs(time.time() - int(timestamp)) > tolerance_seconds:
|
|
33
|
+
return False
|
|
34
|
+
expected = hmac.new(
|
|
35
|
+
secret.encode(), f"{timestamp}.{payload}".encode(), hashlib.sha256
|
|
36
|
+
).hexdigest()
|
|
37
|
+
return hmac.compare_digest(signature, expected)
|
|
38
|
+
except Exception:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _client_version() -> str:
|
|
43
|
+
"""Version reported in the User-Agent header.
|
|
44
|
+
|
|
45
|
+
Read from the installed distribution metadata so it always matches the
|
|
46
|
+
released version; falls back to the source version when running from a
|
|
47
|
+
checkout that was never installed.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
from importlib.metadata import version as _v
|
|
51
|
+
|
|
52
|
+
return _v("gork-sdk")
|
|
53
|
+
except Exception:
|
|
54
|
+
return "0.1.0"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GorkClient:
|
|
58
|
+
def __init__(self, api_key: str = "", base_url: str = DEFAULT_BASE_URL):
|
|
59
|
+
self.api_key = api_key or os.environ.get("GORK_KEY") or os.environ.get("GORK_API_KEY") or ""
|
|
60
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
61
|
+
if not self.api_key:
|
|
62
|
+
import warnings
|
|
63
|
+
|
|
64
|
+
warnings.warn(
|
|
65
|
+
"[gork] initialized without an api_key. Set GORK_KEY or pass api_key explicitly."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _request(
|
|
69
|
+
self,
|
|
70
|
+
method: str,
|
|
71
|
+
path: str,
|
|
72
|
+
body: Optional[Dict[str, Any]] = None,
|
|
73
|
+
query: Optional[Dict[str, Any]] = None,
|
|
74
|
+
headers: Optional[Dict[str, str]] = None,
|
|
75
|
+
) -> Any:
|
|
76
|
+
url = self.base_url + path
|
|
77
|
+
if query:
|
|
78
|
+
url += "?" + urllib.parse.urlencode({k: v for k, v in query.items() if v is not None})
|
|
79
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
80
|
+
req = urllib.request.Request(url, data=data, method=method)
|
|
81
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
82
|
+
req.add_header("Content-Type", "application/json")
|
|
83
|
+
req.add_header("User-Agent", f"gork-sdk-python/{_client_version()}")
|
|
84
|
+
for k, v in (headers or {}).items():
|
|
85
|
+
req.add_header(k, v)
|
|
86
|
+
try:
|
|
87
|
+
with urllib.request.urlopen(req, timeout=30) as res:
|
|
88
|
+
payload = json.loads(res.read().decode() or "{}")
|
|
89
|
+
except urllib.error.HTTPError as e:
|
|
90
|
+
try:
|
|
91
|
+
payload = json.loads(e.read().decode() or "{}")
|
|
92
|
+
except Exception:
|
|
93
|
+
payload = {}
|
|
94
|
+
err = (payload.get("error") or {}) if isinstance(payload, dict) else {}
|
|
95
|
+
raise GorkError(
|
|
96
|
+
err.get("message") or f"API request failed with status {e.code}",
|
|
97
|
+
err.get("code") or "api_error",
|
|
98
|
+
e.code,
|
|
99
|
+
err.get("details"),
|
|
100
|
+
)
|
|
101
|
+
data_key = payload.get("data", payload) if isinstance(payload, dict) else payload
|
|
102
|
+
return data_key
|
|
103
|
+
|
|
104
|
+
def _request_bytes(self, path: str, inline: bool = False) -> bytes:
|
|
105
|
+
"""Fetch a binary endpoint (attachment bytes) rather than JSON."""
|
|
106
|
+
url = self.base_url + path
|
|
107
|
+
if inline:
|
|
108
|
+
url += "?inline=1"
|
|
109
|
+
req = urllib.request.Request(url, method="GET")
|
|
110
|
+
req.add_header("Authorization", f"Bearer {self.api_key}")
|
|
111
|
+
req.add_header("User-Agent", f"gork-sdk-python/{_client_version()}")
|
|
112
|
+
try:
|
|
113
|
+
with urllib.request.urlopen(req, timeout=60) as res:
|
|
114
|
+
return res.read()
|
|
115
|
+
except urllib.error.HTTPError as e:
|
|
116
|
+
try:
|
|
117
|
+
payload = json.loads(e.read().decode() or "{}")
|
|
118
|
+
except Exception:
|
|
119
|
+
payload = {}
|
|
120
|
+
err = (payload.get("error") or {}) if isinstance(payload, dict) else {}
|
|
121
|
+
raise GorkError(
|
|
122
|
+
err.get("message") or f"API request failed with status {e.code}",
|
|
123
|
+
err.get("code") or "api_error",
|
|
124
|
+
e.code,
|
|
125
|
+
err.get("details"),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# ---- Inboxes ----
|
|
129
|
+
def create_inbox(self, username: str = "", name: str = "") -> Dict[str, Any]:
|
|
130
|
+
return self._request("POST", "/v1/inboxes", {"username": username, "name": name})
|
|
131
|
+
|
|
132
|
+
def list_inboxes(self) -> List[Dict[str, Any]]:
|
|
133
|
+
return self._request("GET", "/v1/inboxes")
|
|
134
|
+
|
|
135
|
+
def get_inbox(self, inbox_id: str) -> Dict[str, Any]:
|
|
136
|
+
return self._request("GET", f"/v1/inboxes/{urllib.parse.quote(inbox_id)}")
|
|
137
|
+
|
|
138
|
+
def delete_inbox(self, inbox_id: str) -> Dict[str, Any]:
|
|
139
|
+
return self._request("DELETE", f"/v1/inboxes/{urllib.parse.quote(inbox_id)}")
|
|
140
|
+
|
|
141
|
+
# ---- Messages ----
|
|
142
|
+
def send_message(
|
|
143
|
+
self,
|
|
144
|
+
inbox_id: str,
|
|
145
|
+
to: List[str],
|
|
146
|
+
subject: str = "(no subject)",
|
|
147
|
+
text: str = "",
|
|
148
|
+
html: str = "",
|
|
149
|
+
in_reply_to: str = "",
|
|
150
|
+
idempotency_key: str = "",
|
|
151
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
152
|
+
send_at: str = "",
|
|
153
|
+
track_opens: bool = False,
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
"""Send (or schedule) an outbound email.
|
|
156
|
+
|
|
157
|
+
attachments: [{"filename": "invoice.pdf", "contentType": "application/pdf",
|
|
158
|
+
"content": "<base64>"}] — max 10 files, 10MB each, 15MB total.
|
|
159
|
+
send_at: ISO 8601 timestamp to schedule instead of sending now
|
|
160
|
+
(1 minute to 30 days ahead). No quota is used until it dispatches.
|
|
161
|
+
track_opens: opt in to open tracking (adds a signed pixel, fires
|
|
162
|
+
email.opened on the first open).
|
|
163
|
+
"""
|
|
164
|
+
body: Dict[str, Any] = {"inboxId": inbox_id, "to": to, "subject": subject}
|
|
165
|
+
if text:
|
|
166
|
+
body["text"] = text
|
|
167
|
+
if html:
|
|
168
|
+
body["html"] = html
|
|
169
|
+
if in_reply_to:
|
|
170
|
+
body["inReplyTo"] = in_reply_to
|
|
171
|
+
if attachments:
|
|
172
|
+
body["attachments"] = attachments
|
|
173
|
+
if send_at:
|
|
174
|
+
body["sendAt"] = send_at
|
|
175
|
+
if track_opens:
|
|
176
|
+
body["trackOpens"] = True
|
|
177
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
178
|
+
return self._request("POST", "/v1/messages/send", body, headers=headers)
|
|
179
|
+
|
|
180
|
+
def cancel_scheduled(self, message_id: str) -> Dict[str, Any]:
|
|
181
|
+
"""Cancel a scheduled send before it dispatches."""
|
|
182
|
+
return self._request(
|
|
183
|
+
"DELETE", f"/v1/messages/{urllib.parse.quote(message_id)}/schedule"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def list_messages(
|
|
187
|
+
self,
|
|
188
|
+
inbox_id: str = "",
|
|
189
|
+
thread_id: str = "",
|
|
190
|
+
limit: int = 50,
|
|
191
|
+
q: str = "",
|
|
192
|
+
direction: str = "",
|
|
193
|
+
from_address: str = "",
|
|
194
|
+
since: str = "",
|
|
195
|
+
until: str = "",
|
|
196
|
+
) -> List[Dict[str, Any]]:
|
|
197
|
+
return self._request(
|
|
198
|
+
"GET", "/v1/messages",
|
|
199
|
+
query={
|
|
200
|
+
"inboxId": inbox_id or None,
|
|
201
|
+
"threadId": thread_id or None,
|
|
202
|
+
"limit": limit,
|
|
203
|
+
"q": q or None,
|
|
204
|
+
"direction": direction or None,
|
|
205
|
+
"from": from_address or None,
|
|
206
|
+
"since": since or None,
|
|
207
|
+
"until": until or None,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def search_messages(self, query: str, **filters: Any) -> List[Dict[str, Any]]:
|
|
212
|
+
"""Search subject, body, sender and recipients (newest first)."""
|
|
213
|
+
return self.list_messages(q=query, **filters)
|
|
214
|
+
|
|
215
|
+
def download_attachment(self, attachment_id: str, inline: bool = False) -> bytes:
|
|
216
|
+
"""Download an attachment's bytes. Ids come from a message's `attachments`."""
|
|
217
|
+
return self._request_bytes(
|
|
218
|
+
f"/v1/attachments/{urllib.parse.quote(attachment_id)}", inline=inline
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# ---- Drafts (compose without sending; no quota used) ----
|
|
222
|
+
def create_draft(
|
|
223
|
+
self,
|
|
224
|
+
inbox_id: str,
|
|
225
|
+
to: Optional[List[str]] = None,
|
|
226
|
+
subject: str = "",
|
|
227
|
+
text: str = "",
|
|
228
|
+
html: str = "",
|
|
229
|
+
in_reply_to: str = "",
|
|
230
|
+
) -> Dict[str, Any]:
|
|
231
|
+
body: Dict[str, Any] = {"inboxId": inbox_id}
|
|
232
|
+
if to:
|
|
233
|
+
body["to"] = to
|
|
234
|
+
if subject:
|
|
235
|
+
body["subject"] = subject
|
|
236
|
+
if text:
|
|
237
|
+
body["text"] = text
|
|
238
|
+
if html:
|
|
239
|
+
body["html"] = html
|
|
240
|
+
if in_reply_to:
|
|
241
|
+
body["inReplyTo"] = in_reply_to
|
|
242
|
+
return self._request("POST", "/v1/drafts", body)
|
|
243
|
+
|
|
244
|
+
def list_drafts(
|
|
245
|
+
self, inbox_id: str = "", status: str = "draft", limit: int = 50
|
|
246
|
+
) -> List[Dict[str, Any]]:
|
|
247
|
+
return self._request(
|
|
248
|
+
"GET", "/v1/drafts",
|
|
249
|
+
query={
|
|
250
|
+
"inboxId": inbox_id or None,
|
|
251
|
+
"status": status or None,
|
|
252
|
+
"limit": limit,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def get_draft(self, draft_id: str) -> Dict[str, Any]:
|
|
257
|
+
return self._request("GET", f"/v1/drafts/{urllib.parse.quote(draft_id)}")
|
|
258
|
+
|
|
259
|
+
def update_draft(self, draft_id: str, **fields: Any) -> Dict[str, Any]:
|
|
260
|
+
"""Update draft fields, e.g. update_draft(id, subject="Hi", to=["a@b.com"])."""
|
|
261
|
+
return self._request(
|
|
262
|
+
"PATCH", f"/v1/drafts/{urllib.parse.quote(draft_id)}", fields
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def discard_draft(self, draft_id: str) -> Dict[str, Any]:
|
|
266
|
+
return self._request("DELETE", f"/v1/drafts/{urllib.parse.quote(draft_id)}")
|
|
267
|
+
|
|
268
|
+
def send_draft(self, draft_id: str) -> Dict[str, Any]:
|
|
269
|
+
"""Send a draft through the full pipeline. Idempotent per draft."""
|
|
270
|
+
return self._request(
|
|
271
|
+
"POST", f"/v1/drafts/{urllib.parse.quote(draft_id)}/send"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def get_message(self, message_id: str) -> Dict[str, Any]:
|
|
275
|
+
return self._request("GET", f"/v1/messages/{urllib.parse.quote(message_id)}")
|
|
276
|
+
|
|
277
|
+
# ---- Threads ----
|
|
278
|
+
def list_threads(self) -> List[Dict[str, Any]]:
|
|
279
|
+
return self._request("GET", "/v1/threads")
|
|
280
|
+
|
|
281
|
+
def get_thread(self, thread_id: str) -> Dict[str, Any]:
|
|
282
|
+
return self._request("GET", f"/v1/threads/{urllib.parse.quote(thread_id)}")
|
|
283
|
+
|
|
284
|
+
# ---- Webhooks ----
|
|
285
|
+
def create_webhook(self, url: str, events: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
286
|
+
return self._request(
|
|
287
|
+
"POST", "/v1/webhooks",
|
|
288
|
+
{"url": url, "subscribedEvents": events or ["email.received", "email.sent", "email.bounced"]},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def list_webhooks(self) -> List[Dict[str, Any]]:
|
|
292
|
+
return self._request("GET", "/v1/webhooks")
|
|
293
|
+
|
|
294
|
+
def rotate_webhook_secret(self, webhook_id: str) -> Dict[str, Any]:
|
|
295
|
+
return self._request("POST", f"/v1/webhooks/{urllib.parse.quote(webhook_id)}/rotate-secret")
|
|
296
|
+
|
|
297
|
+
def delete_webhook(self, webhook_id: str) -> Dict[str, Any]:
|
|
298
|
+
return self._request("DELETE", f"/v1/webhooks/{urllib.parse.quote(webhook_id)}")
|
|
299
|
+
|
|
300
|
+
def list_deliveries(self, limit: int = 50) -> List[Dict[str, Any]]:
|
|
301
|
+
return self._request("GET", "/v1/webhooks/deliveries", query={"limit": limit})
|
|
302
|
+
|
|
303
|
+
# ---- API keys ----
|
|
304
|
+
def create_api_key(self, name: str, scopes: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
305
|
+
return self._request("POST", "/v1/keys", {"name": name, "scopes": scopes or ["*"]})
|
|
306
|
+
|
|
307
|
+
def list_api_keys(self) -> List[Dict[str, Any]]:
|
|
308
|
+
return self._request("GET", "/v1/keys")
|
|
309
|
+
|
|
310
|
+
def revoke_api_key(self, key_id: str) -> Dict[str, Any]:
|
|
311
|
+
return self._request("DELETE", f"/v1/keys/{urllib.parse.quote(key_id)}")
|
|
312
|
+
|
|
313
|
+
# ---- Domains ----
|
|
314
|
+
def create_domain(self, domain: str) -> Dict[str, Any]:
|
|
315
|
+
return self._request("POST", "/v1/domains", {"domain": domain})
|
|
316
|
+
|
|
317
|
+
def list_domains(self) -> List[Dict[str, Any]]:
|
|
318
|
+
return self._request("GET", "/v1/domains")
|
|
319
|
+
|
|
320
|
+
def verify_domain(self, domain_id: str) -> Dict[str, Any]:
|
|
321
|
+
return self._request("POST", f"/v1/domains/{urllib.parse.quote(domain_id)}/verify")
|
|
322
|
+
|
|
323
|
+
def delete_domain(self, domain_id: str) -> Dict[str, Any]:
|
|
324
|
+
return self._request("DELETE", f"/v1/domains/{urllib.parse.quote(domain_id)}")
|
|
325
|
+
|
|
326
|
+
# ---- Suppressions ----
|
|
327
|
+
def list_suppressions(self) -> List[Dict[str, Any]]:
|
|
328
|
+
return self._request("GET", "/v1/suppressions")
|
|
329
|
+
|
|
330
|
+
def suppress(self, email: str, reason: str = "manual") -> Dict[str, Any]:
|
|
331
|
+
return self._request("POST", "/v1/suppressions", {"email": email, "reason": reason})
|
|
332
|
+
|
|
333
|
+
def unsuppress(self, suppression_id: str) -> Dict[str, Any]:
|
|
334
|
+
return self._request("DELETE", f"/v1/suppressions/{urllib.parse.quote(suppression_id)}")
|
|
335
|
+
|
|
336
|
+
# ---- AI Agent Tools (OpenAI / Anthropic / LangChain / CrewAI compatible) ----
|
|
337
|
+
def get_tools(self) -> List[Dict[str, Any]]:
|
|
338
|
+
"""Return standard function-calling tool schemas for OpenAI, Anthropic, or LangChain."""
|
|
339
|
+
return [
|
|
340
|
+
{
|
|
341
|
+
"type": "function",
|
|
342
|
+
"function": {
|
|
343
|
+
"name": "gork_create_inbox",
|
|
344
|
+
"description": "Provision a new programmable email inbox for an AI agent.",
|
|
345
|
+
"parameters": {
|
|
346
|
+
"type": "object",
|
|
347
|
+
"properties": {
|
|
348
|
+
"username": {"type": "string", "description": "Lowercase username, e.g. sdr-alex"},
|
|
349
|
+
"name": {"type": "string", "description": "Friendly agent display name"},
|
|
350
|
+
},
|
|
351
|
+
"required": ["username"],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"type": "function",
|
|
357
|
+
"function": {
|
|
358
|
+
"name": "gork_list_inboxes",
|
|
359
|
+
"description": "List all email inboxes available to the organization.",
|
|
360
|
+
"parameters": {"type": "object", "properties": {}},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"type": "function",
|
|
365
|
+
"function": {
|
|
366
|
+
"name": "gork_send_email",
|
|
367
|
+
"description": "Send an outbound email from an agent inbox.",
|
|
368
|
+
"parameters": {
|
|
369
|
+
"type": "object",
|
|
370
|
+
"properties": {
|
|
371
|
+
"inbox_id": {"type": "string", "description": "The ID of the inbox to send from"},
|
|
372
|
+
"to": {"type": "array", "items": {"type": "string"}, "description": "Recipient email addresses"},
|
|
373
|
+
"subject": {"type": "string", "description": "Email subject line"},
|
|
374
|
+
"text": {"type": "string", "description": "Plain text email body"},
|
|
375
|
+
},
|
|
376
|
+
"required": ["inbox_id", "to", "subject", "text"],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"type": "function",
|
|
382
|
+
"function": {
|
|
383
|
+
"name": "gork_read_emails",
|
|
384
|
+
"description": "List or search messages, optionally filtered by inbox.",
|
|
385
|
+
"parameters": {
|
|
386
|
+
"type": "object",
|
|
387
|
+
"properties": {
|
|
388
|
+
"inbox_id": {"type": "string", "description": "Filter by inbox ID"},
|
|
389
|
+
"limit": {"type": "integer", "description": "Max messages to return (default 20)"},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"type": "function",
|
|
396
|
+
"function": {
|
|
397
|
+
"name": "gork_get_message",
|
|
398
|
+
"description": "Retrieve full details and content of a specific email message.",
|
|
399
|
+
"parameters": {
|
|
400
|
+
"type": "object",
|
|
401
|
+
"properties": {
|
|
402
|
+
"message_id": {"type": "string", "description": "The ID of the message (msg_...)"},
|
|
403
|
+
},
|
|
404
|
+
"required": ["message_id"],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"type": "function",
|
|
410
|
+
"function": {
|
|
411
|
+
"name": "gork_create_draft",
|
|
412
|
+
"description": "Create an email draft for review before sending.",
|
|
413
|
+
"parameters": {
|
|
414
|
+
"type": "object",
|
|
415
|
+
"properties": {
|
|
416
|
+
"inbox_id": {"type": "string", "description": "Inbox ID"},
|
|
417
|
+
"to": {"type": "array", "items": {"type": "string"}, "description": "Recipient addresses"},
|
|
418
|
+
"subject": {"type": "string", "description": "Subject line"},
|
|
419
|
+
"text": {"type": "string", "description": "Plain text body"},
|
|
420
|
+
},
|
|
421
|
+
"required": ["inbox_id", "to", "subject", "text"],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"type": "function",
|
|
427
|
+
"function": {
|
|
428
|
+
"name": "gork_send_draft",
|
|
429
|
+
"description": "Send an existing email draft.",
|
|
430
|
+
"parameters": {
|
|
431
|
+
"type": "object",
|
|
432
|
+
"properties": {
|
|
433
|
+
"draft_id": {"type": "string", "description": "Draft ID (drf_...)"},
|
|
434
|
+
},
|
|
435
|
+
"required": ["draft_id"],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
def execute_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
|
|
442
|
+
"""Execute a tool called by an LLM function call."""
|
|
443
|
+
if name == "gork_create_inbox":
|
|
444
|
+
return self.create_inbox(arguments["username"], name=arguments.get("name"))
|
|
445
|
+
elif name == "gork_list_inboxes":
|
|
446
|
+
return self.list_inboxes()
|
|
447
|
+
elif name == "gork_send_email":
|
|
448
|
+
return self.send_message(
|
|
449
|
+
inbox_id=arguments["inbox_id"],
|
|
450
|
+
to=arguments["to"],
|
|
451
|
+
subject=arguments["subject"],
|
|
452
|
+
text=arguments["text"],
|
|
453
|
+
)
|
|
454
|
+
elif name == "gork_read_emails":
|
|
455
|
+
return self.list_messages(inbox_id=arguments.get("inbox_id"), limit=arguments.get("limit", 20))
|
|
456
|
+
elif name == "gork_get_message":
|
|
457
|
+
return self.get_message(arguments["message_id"])
|
|
458
|
+
elif name == "gork_create_draft":
|
|
459
|
+
return self.create_draft(
|
|
460
|
+
inbox_id=arguments["inbox_id"],
|
|
461
|
+
to=arguments["to"],
|
|
462
|
+
subject=arguments["subject"],
|
|
463
|
+
text=arguments["text"],
|
|
464
|
+
)
|
|
465
|
+
elif name == "gork_send_draft":
|
|
466
|
+
return self.send_draft(arguments["draft_id"])
|
|
467
|
+
else:
|
|
468
|
+
raise ValueError(f"Unknown Gork tool: {name}")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Error type carrying the API's stable error code."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GorkError(Exception):
|
|
5
|
+
def __init__(self, message: str, code: str = "api_error", status: int = 0, details=None):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
self.code = code
|
|
8
|
+
self.status = status
|
|
9
|
+
self.details = details
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""TypedDict shapes mirroring the gork.email API envelopes."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Inbox(TypedDict, total=False):
|
|
7
|
+
id: str
|
|
8
|
+
address: str
|
|
9
|
+
name: Optional[str]
|
|
10
|
+
isActive: bool
|
|
11
|
+
createdAt: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Message(TypedDict, total=False):
|
|
15
|
+
id: str
|
|
16
|
+
threadId: str
|
|
17
|
+
inboxId: str
|
|
18
|
+
direction: str
|
|
19
|
+
fromAddress: str
|
|
20
|
+
subject: str
|
|
21
|
+
textBody: Optional[str]
|
|
22
|
+
status: str
|
|
23
|
+
createdAt: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Thread(TypedDict, total=False):
|
|
27
|
+
id: str
|
|
28
|
+
subject: str
|
|
29
|
+
messageCount: int
|
|
30
|
+
lastMessageAt: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Webhook(TypedDict, total=False):
|
|
34
|
+
id: str
|
|
35
|
+
url: str
|
|
36
|
+
subscribedEvents: List[str]
|
|
37
|
+
isActive: bool
|
|
38
|
+
secret: Optional[str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ApiKey(TypedDict, total=False):
|
|
42
|
+
id: str
|
|
43
|
+
name: str
|
|
44
|
+
keyPrefix: str
|
|
45
|
+
apiKey: Optional[str]
|
|
46
|
+
scopes: List[str]
|
|
47
|
+
createdAt: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Domain(TypedDict, total=False):
|
|
51
|
+
id: str
|
|
52
|
+
domainName: str
|
|
53
|
+
status: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Suppression(TypedDict, total=False):
|
|
57
|
+
id: str
|
|
58
|
+
email: str
|
|
59
|
+
reason: str
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gork-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for gork.email programmable email infrastructure for AI agents
|
|
5
|
+
Author: gork.email
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gork.email
|
|
8
|
+
Project-URL: Documentation, https://gork.email/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/shyamcxy/gork-sdk-python
|
|
10
|
+
Project-URL: Issues, https://github.com/shyamcxy/gork-sdk-python/issues
|
|
11
|
+
Keywords: email,agent,ai,mcp,inbox,mail
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# gork-sdk — Python SDK for gork.email
|
|
20
|
+
|
|
21
|
+
> **Pre-alpha (0.1.0).** gork.email is in active development and the API may change between 0.x releases without a major bump. Pin an exact version in production.
|
|
22
|
+
|
|
23
|
+
Programmable email infrastructure for AI agents. Provision inboxes, send and
|
|
24
|
+
receive mail, search a mailbox, compose drafts, schedule sends, read
|
|
25
|
+
attachments, and manage threads, webhooks, domains, keys, and suppressions.
|
|
26
|
+
|
|
27
|
+
This SDK tracks the REST API at `https://api.gork.email/v1` and the TypeScript
|
|
28
|
+
SDK (`@gork/sdk`); the method names and parameters match.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install gork-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from gork import GorkClient
|
|
40
|
+
|
|
41
|
+
gork = GorkClient() # reads GORK_KEY / GORK_API_KEY
|
|
42
|
+
|
|
43
|
+
inbox = gork.create_inbox(username="sdr-alex", name="SDR Alex")
|
|
44
|
+
print(inbox["address"]) # sdr-alex@try.gork.email
|
|
45
|
+
|
|
46
|
+
gork.send_message(
|
|
47
|
+
inbox_id=inbox["id"],
|
|
48
|
+
to=["prospect@acme.com"],
|
|
49
|
+
subject="Quick intro",
|
|
50
|
+
text="Hi — wanted to share what we're building.",
|
|
51
|
+
idempotency_key="outreach-001", # safe retries
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Webhook verification
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from gork import verify_signature
|
|
59
|
+
|
|
60
|
+
ok = verify_signature(raw_body, request.headers["X-Gork-Signature"], webhook_secret)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Links
|
|
64
|
+
|
|
65
|
+
- Docs: https://gork.email/docs
|
|
66
|
+
- API reference: https://api.gork.email/openapi.json
|
|
67
|
+
- Changelog: https://gork.email/changelog
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gork/__init__.py
|
|
4
|
+
src/gork/client.py
|
|
5
|
+
src/gork/errors.py
|
|
6
|
+
src/gork/types.py
|
|
7
|
+
src/gork_sdk.egg-info/PKG-INFO
|
|
8
|
+
src/gork_sdk.egg-info/SOURCES.txt
|
|
9
|
+
src/gork_sdk.egg-info/dependency_links.txt
|
|
10
|
+
src/gork_sdk.egg-info/top_level.txt
|
|
11
|
+
tests/test_client_requests.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gork
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Smoke test: the new methods build the right requests (no network)."""
|
|
2
|
+
import json, sys, urllib.request
|
|
3
|
+
sys.path.insert(0, "src")
|
|
4
|
+
from gork import GorkClient
|
|
5
|
+
|
|
6
|
+
calls = []
|
|
7
|
+
|
|
8
|
+
class FakeResponse:
|
|
9
|
+
def __init__(self, payload): self._payload = payload
|
|
10
|
+
def read(self): return json.dumps(self._payload).encode()
|
|
11
|
+
def __enter__(self): return self
|
|
12
|
+
def __exit__(self, *a): return False
|
|
13
|
+
|
|
14
|
+
def fake_urlopen(req, timeout=None):
|
|
15
|
+
calls.append({
|
|
16
|
+
"url": req.full_url,
|
|
17
|
+
"method": req.get_method(),
|
|
18
|
+
"body": json.loads(req.data.decode()) if req.data else None,
|
|
19
|
+
"headers": dict(req.headers),
|
|
20
|
+
})
|
|
21
|
+
if "attachments" in req.full_url and "/v1/attachments/" in req.full_url:
|
|
22
|
+
return FakeResponseBytes(b"FILE-BYTES")
|
|
23
|
+
return FakeResponse({"data": {"id": "msg_1", "status": "delivered"}})
|
|
24
|
+
|
|
25
|
+
class FakeResponseBytes:
|
|
26
|
+
def __init__(self, b): self._b = b
|
|
27
|
+
def read(self): return self._b
|
|
28
|
+
def __enter__(self): return self
|
|
29
|
+
def __exit__(self, *a): return False
|
|
30
|
+
|
|
31
|
+
urllib.request.urlopen = fake_urlopen
|
|
32
|
+
client = GorkClient(api_key="gork_live_test")
|
|
33
|
+
|
|
34
|
+
# attachments + tracking on send
|
|
35
|
+
client.send_message("inb_1", ["a@b.com"], subject="Invoice", text="hi",
|
|
36
|
+
attachments=[{"filename": "i.pdf", "contentType": "application/pdf", "content": "UERG"}],
|
|
37
|
+
track_opens=True)
|
|
38
|
+
body = calls[-1]["body"]
|
|
39
|
+
assert body["attachments"][0]["filename"] == "i.pdf", body
|
|
40
|
+
assert body["trackOpens"] is True
|
|
41
|
+
|
|
42
|
+
# scheduling
|
|
43
|
+
client.send_message("inb_1", ["a@b.com"], subject="Later", send_at="2026-09-12T10:00:00.000Z")
|
|
44
|
+
assert calls[-1]["body"]["sendAt"] == "2026-09-12T10:00:00.000Z"
|
|
45
|
+
|
|
46
|
+
# search filters reach the query string
|
|
47
|
+
client.search_messages("invoice", direction="inbound")
|
|
48
|
+
assert "q=invoice" in calls[-1]["url"], calls[-1]["url"]
|
|
49
|
+
assert "direction=inbound" in calls[-1]["url"]
|
|
50
|
+
|
|
51
|
+
# draft lifecycle
|
|
52
|
+
client.create_draft("inb_1", to=["a@b.com"], subject="Draft")
|
|
53
|
+
assert calls[-1]["method"] == "POST" and calls[-1]["url"].endswith("/v1/drafts")
|
|
54
|
+
client.update_draft("drf_1", subject="Edited")
|
|
55
|
+
assert calls[-1]["method"] == "PATCH"
|
|
56
|
+
client.send_draft("drf_1")
|
|
57
|
+
assert calls[-1]["url"].endswith("/v1/drafts/drf_1/send")
|
|
58
|
+
client.discard_draft("drf_1")
|
|
59
|
+
assert calls[-1]["method"] == "DELETE"
|
|
60
|
+
|
|
61
|
+
# attachment bytes
|
|
62
|
+
data = client.download_attachment("att_1")
|
|
63
|
+
assert data == b"FILE-BYTES", data
|
|
64
|
+
|
|
65
|
+
# cancel a schedule
|
|
66
|
+
client.cancel_scheduled("msg_1")
|
|
67
|
+
assert calls[-1]["method"] == "DELETE" and calls[-1]["url"].endswith("/schedule")
|
|
68
|
+
|
|
69
|
+
# AI agent function tools
|
|
70
|
+
tools = client.get_tools()
|
|
71
|
+
assert len(tools) >= 6
|
|
72
|
+
assert any(t["function"]["name"] == "gork_send_email" for t in tools)
|
|
73
|
+
client.execute_tool("gork_send_email", {
|
|
74
|
+
"inbox_id": "inb_1",
|
|
75
|
+
"to": ["ai@example.com"],
|
|
76
|
+
"subject": "Agent Hello",
|
|
77
|
+
"text": "Sent via tool",
|
|
78
|
+
})
|
|
79
|
+
assert calls[-1]["body"]["to"] == ["ai@example.com"]
|
|
80
|
+
|
|
81
|
+
print("python SDK smoke test passed:", len(calls), "calls checked")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── Version drift guard ────────────────────────────────────────────────────
|
|
85
|
+
# The User-Agent header used to hardcode a version string; it now reads the
|
|
86
|
+
# installed distribution metadata, and these keep it honest and pre-alpha.
|
|
87
|
+
def test_version_matches_pyproject():
|
|
88
|
+
import re, pathlib
|
|
89
|
+
root = pathlib.Path(__file__).resolve().parent.parent
|
|
90
|
+
declared = re.search(r'^version = "([^"]+)"', (root / "pyproject.toml").read_text(), re.M).group(1)
|
|
91
|
+
assert declared == "0.1.0", f"expected the 0.x pre-alpha line, got {declared}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_user_agent_uses_the_helper():
|
|
95
|
+
import pathlib
|
|
96
|
+
src = (pathlib.Path(__file__).resolve().parent.parent / "src" / "gork" / "client.py").read_text()
|
|
97
|
+
assert "gork-sdk-python/{_client_version()}" in src
|
|
98
|
+
assert not __import__("re").search(r'gork-sdk-python/\d', src)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_reported_version_is_pre_alpha():
|
|
102
|
+
import sys, pathlib
|
|
103
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
|
|
104
|
+
from gork.client import _client_version
|
|
105
|
+
assert _client_version().startswith("0.")
|
|
106
|
+
|
|
107
|
+
test_version_matches_pyproject()
|
|
108
|
+
test_user_agent_uses_the_helper()
|
|
109
|
+
test_reported_version_is_pre_alpha()
|
|
110
|
+
print("all version drift guards passed")
|