domani 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,71 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+ /dist
23
+
24
+ # misc
25
+ .DS_Store
26
+ *.pem
27
+
28
+ # debug
29
+ npm-debug.log*
30
+ yarn-debug.log*
31
+ yarn-error.log*
32
+ .pnpm-debug.log*
33
+
34
+ # env files
35
+ .env
36
+ .env.local
37
+ .env.*.local
38
+ .env.production
39
+ .env.preview
40
+ *.bak
41
+
42
+ # cli build
43
+ cli/dist
44
+ cli/node_modules
45
+ cli/.next
46
+ public/cli/domainer.js
47
+ public/cli/domainer.js.sha256
48
+
49
+ # sdk build artifacts (generated SOURCES are committed; these are not)
50
+ sdk/openapi.json
51
+ sdk/ts/dist
52
+ sdk/ts/node_modules
53
+ sdk/ts/package-lock.json
54
+ sdk/python/dist
55
+ sdk/python/build
56
+ sdk/**/__pycache__
57
+ sdk/**/*.egg-info
58
+
59
+ # generated skill
60
+ public/skill.md
61
+ public/references/
62
+
63
+ # vercel
64
+ .vercel
65
+
66
+ # typescript
67
+ *.tsbuildinfo
68
+ next-env.d.ts
69
+
70
+ /src/generated/prisma
71
+ .env*.local
domani-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: domani
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the domani API - domains, email, and identity for AI agents.
5
+ Project-URL: Homepage, https://domani.run
6
+ Project-URL: Repository, https://github.com/gwendall/domani
7
+ Author: domani
8
+ License: MIT
9
+ Keywords: agents,api,domains,domani,email,mcp,sdk
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # domani
17
+
18
+ Official Python SDK for the [domani](https://domani.run) API - domains, email, and identity for AI agents. Typed, generated from the OpenAPI spec, standard-library only (no dependencies).
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install domani
24
+ ```
25
+
26
+ Requires Python 3.8+.
27
+
28
+ ## Quickstart
29
+
30
+ ```python
31
+ from domani import Domani, DomaniError
32
+
33
+ domani = Domani(api_key="domani_sk_...")
34
+
35
+ # Search availability + pricing
36
+ results = domani.search(query={"q": "myagent.com"})
37
+
38
+ # Give an agent an inbox
39
+ domani.create_mailbox_by_address(body={"address": "hi@myagent.com"})
40
+
41
+ # Send mail
42
+ domani.send_email_by_address(
43
+ "hi@myagent.com",
44
+ body={"to": "user@example.com", "subject": "Hello", "text": "Sent from my domani mailbox."},
45
+ )
46
+
47
+ # Errors carry the API's structured code + hint
48
+ try:
49
+ domani.send_email_by_address("hi@myagent.com", body={"to": "bounced@example.com", "text": "..."})
50
+ except DomaniError as e:
51
+ print(e.status, e.code, e.hint) # e.g. 400 RECIPIENTS_SUPPRESSED
52
+ ```
53
+
54
+ ## API
55
+
56
+ `Domani(api_key, base_url=..., timeout=...)` exposes one method per API operation (named after its operationId in snake_case). Path parameters are positional; request bodies (`body=`) and query objects (`query=`) are keyword arguments. Every method returns the parsed JSON response and raises `DomaniError` (`status`, `code`, `hint`, `documentation_url`) on non-2xx.
57
+
58
+ Full endpoint reference: <https://domani.run/docs> · OpenAPI spec: <https://domani.run/.well-known/openapi.json>
59
+
60
+ ## Generated
61
+
62
+ `domani/__init__.py` is generated from the domani OpenAPI spec - do not edit it by hand. It is regenerated whenever the API surface changes.
63
+
64
+ ## License
65
+
66
+ MIT
domani-0.1.0/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # domani
2
+
3
+ Official Python SDK for the [domani](https://domani.run) API - domains, email, and identity for AI agents. Typed, generated from the OpenAPI spec, standard-library only (no dependencies).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install domani
9
+ ```
10
+
11
+ Requires Python 3.8+.
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ from domani import Domani, DomaniError
17
+
18
+ domani = Domani(api_key="domani_sk_...")
19
+
20
+ # Search availability + pricing
21
+ results = domani.search(query={"q": "myagent.com"})
22
+
23
+ # Give an agent an inbox
24
+ domani.create_mailbox_by_address(body={"address": "hi@myagent.com"})
25
+
26
+ # Send mail
27
+ domani.send_email_by_address(
28
+ "hi@myagent.com",
29
+ body={"to": "user@example.com", "subject": "Hello", "text": "Sent from my domani mailbox."},
30
+ )
31
+
32
+ # Errors carry the API's structured code + hint
33
+ try:
34
+ domani.send_email_by_address("hi@myagent.com", body={"to": "bounced@example.com", "text": "..."})
35
+ except DomaniError as e:
36
+ print(e.status, e.code, e.hint) # e.g. 400 RECIPIENTS_SUPPRESSED
37
+ ```
38
+
39
+ ## API
40
+
41
+ `Domani(api_key, base_url=..., timeout=...)` exposes one method per API operation (named after its operationId in snake_case). Path parameters are positional; request bodies (`body=`) and query objects (`query=`) are keyword arguments. Every method returns the parsed JSON response and raises `DomaniError` (`status`, `code`, `hint`, `documentation_url`) on non-2xx.
42
+
43
+ Full endpoint reference: <https://domani.run/docs> · OpenAPI spec: <https://domani.run/.well-known/openapi.json>
44
+
45
+ ## Generated
46
+
47
+ `domani/__init__.py` is generated from the domani OpenAPI spec - do not edit it by hand. It is regenerated whenever the API surface changes.
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,763 @@
1
+ # AUTO-GENERATED from the domani OpenAPI spec by scripts/generate-sdk.ts.
2
+ # Do not edit by hand - re-run `npx tsx scripts/generate-sdk.ts`.
3
+ """Typed Python client for the domani API."""
4
+ from __future__ import annotations
5
+
6
+ import json as _json
7
+ import urllib.parse
8
+ import urllib.request
9
+ import urllib.error
10
+ from typing import Any, Dict, List, Literal, Optional, Union
11
+ from urllib.parse import quote
12
+
13
+ try: # Python 3.8+
14
+ from typing import TypedDict
15
+ except ImportError: # pragma: no cover
16
+ from typing_extensions import TypedDict # type: ignore
17
+
18
+ DEFAULT_BASE_URL = "https://domani.run"
19
+
20
+
21
+ class DomaniError(Exception):
22
+ """Structured API error - mirrors the apiError() shape returned by the API."""
23
+
24
+ def __init__(self, status: int, body: Any):
25
+ self.status = status
26
+ if isinstance(body, dict):
27
+ self.code = body.get("code", "ERROR")
28
+ self.hint = body.get("hint")
29
+ self.documentation_url = body.get("documentation_url")
30
+ message = body.get("error") or body.get("message") or f"Request failed with status {status}"
31
+ else:
32
+ self.code = "ERROR"
33
+ self.hint = None
34
+ self.documentation_url = None
35
+ message = f"Request failed with status {status}"
36
+ super().__init__(message)
37
+
38
+
39
+ # ─── Types ──────────────────────────────────────────
40
+
41
+ class Error(TypedDict, total=False):
42
+ error: str
43
+ code: Literal["MISSING_API_KEY", "INVALID_API_KEY", "EXPIRED_API_KEY", "MISSING_PARAMETER", "INVALID_DOMAIN", "PAYMENT_REQUIRED", "DOMAIN_UNAVAILABLE", "RATE_LIMIT_EXCEEDED", "USER_EXISTS", "NOT_FOUND", "UNKNOWN_PROVIDER", "UNKNOWN_METHOD", "TARGET_REQUIRED", "INVALID_PARAMETER", "CONTACT_REQUIRED", "INVALID_CONTACT", "INSUFFICIENT_SCOPE", "SCOPE_ESCALATION", "DOMAIN_EXISTS", "DOMAIN_NOT_ACTIVE", "UNSUPPORTED_TLD", "NOT_AVAILABLE", "ALREADY_REGISTERED", "TRANSFER_NOT_ELIGIBLE", "ALREADY_LISTED", "LISTING_SOLD", "SELF_PURCHASE", "DESCRIPTION_TOO_LONG", "DEAL_NOT_FOUND", "INVALID_DEAL_STATE", "EPP_ATTEMPTS_EXCEEDED", "ACTIVE_DEAL_EXISTS"]
44
+ hint: str
45
+ fix_command: str
46
+ documentation_url: str
47
+ setup_url: str
48
+ fix_url: str
49
+ retry_after: int
50
+
51
+
52
+ class SearchResult(TypedDict, total=False):
53
+ available: bool
54
+ domain: str
55
+ price: float
56
+ currency: str
57
+ error: bool
58
+ for_sale: Dict[str, Any]
59
+
60
+
61
+ class TldInfo(TypedDict, total=False):
62
+ tld: str
63
+ registration: float
64
+ renewal: float
65
+ transfer: float
66
+
67
+
68
+ class DnsRecord(TypedDict, total=False):
69
+ type: Literal["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA"]
70
+ name: str
71
+ value: str
72
+ ttl: int
73
+ priority: int
74
+
75
+
76
+ class DomainRecord(TypedDict, total=False):
77
+ domain: str
78
+ status: Literal["active", "expired", "pending"]
79
+ auto_renew: bool
80
+ purchased_at: str
81
+ expires_at: str
82
+ email_enabled: bool
83
+ email_verified: bool
84
+ email_verified_at: Optional[str]
85
+ mailbox_count: int
86
+
87
+
88
+ class Backorder(TypedDict, total=False):
89
+ id: str
90
+ domain: str
91
+ status: Literal["watching", "caught", "failed", "cancelled", "expired"]
92
+ payment_method: Literal["card", "balance"]
93
+ max_price: float
94
+ currency: str
95
+ domain_expires: Optional[str]
96
+ last_checked: Optional[str]
97
+ attempts: int
98
+ caught_at: Optional[str]
99
+ registered_domain_id: Optional[str]
100
+ failure_reason: Optional[str]
101
+ expires_at: str
102
+ created_at: str
103
+
104
+
105
+ class DealInvoice(TypedDict, total=False):
106
+ deal_id: str
107
+ domain: str
108
+ date: str
109
+ status: str
110
+ your_role: Literal["buyer", "seller"]
111
+ currency: str
112
+ line_items: List[Dict[str, Any]]
113
+ total: float
114
+ commission_rate: float
115
+ commission_amount: float
116
+ payment_method: str
117
+ payout_method: Literal["bank", "balance"]
118
+ payout_status: Literal["paid", "credited", "pending"]
119
+
120
+
121
+ class BrokerRequest(TypedDict, total=False):
122
+ id: str
123
+ domain: str
124
+ status: Literal["sourcing", "contacted", "negotiating", "agreed", "completed", "no_contact", "declined", "expired", "cancelled"]
125
+ max_budget: Optional[float]
126
+ contacted: bool
127
+ negotiation_id: Optional[str]
128
+ failure_reason: Optional[str]
129
+ expires_at: str
130
+ created_at: str
131
+
132
+
133
+ class Negotiation(TypedDict, total=False):
134
+ id: str
135
+ domain: str
136
+ status: Literal["open", "agreed", "completed", "declined", "expired", "cancelled"]
137
+ your_role: Literal["buyer", "seller"]
138
+ your_turn: bool
139
+ current_offer: float
140
+ agreed_price: Optional[float]
141
+ list_price: float
142
+ expires_at: str
143
+ deal_id: Optional[str]
144
+ created_at: str
145
+ offers: List[Dict[str, Any]]
146
+
147
+
148
+ class DomainDetail(TypedDict, total=False):
149
+ domain: str
150
+ status: Literal["active", "expired", "pending"]
151
+ auto_renew: bool
152
+ purchased_at: str
153
+ expires_at: str
154
+ days_until_expiry: int
155
+ payment_method: Literal["card", "x402", "crypto"]
156
+ payment_tx: Optional[str]
157
+ registrar: Optional[Dict[str, Any]]
158
+
159
+
160
+ class TokenInfo(TypedDict, total=False):
161
+ id: str
162
+ name: str
163
+ key: str
164
+ last_used: Optional[str]
165
+ expires_at: Optional[str]
166
+ expired: bool
167
+ scopes: List[str]
168
+ created_at: str
169
+
170
+
171
+ class Contact(TypedDict, total=False):
172
+ name: Optional[str]
173
+ organization: Optional[str]
174
+ email: Optional[str]
175
+ phone: Optional[str]
176
+ fax: Optional[str]
177
+ street: Optional[str]
178
+ city: Optional[str]
179
+ state: Optional[str]
180
+ postal_code: Optional[str]
181
+ country: Optional[str]
182
+
183
+
184
+ class RegistrantContact(TypedDict, total=False):
185
+ first_name: str
186
+ last_name: str
187
+ org_name: Optional[str]
188
+ address1: str
189
+ address2: Optional[str]
190
+ city: str
191
+ state: str
192
+ postal_code: str
193
+ country: str
194
+ phone: str
195
+ email: str
196
+
197
+
198
+ class AccountInfo(TypedDict, total=False):
199
+ id: str
200
+ email: str
201
+ has_payment_method: bool
202
+ has_contact: bool
203
+ contact: "RegistrantContact"
204
+ referral_code: str
205
+ referral_rate: int
206
+ balance: float
207
+ domain_count: int
208
+ token_count: int
209
+ created_at: str
210
+
211
+
212
+ # ─── Client ─────────────────────────────────────────
213
+
214
+ class Domani:
215
+ """domani API client. Get an API key at https://domani.run."""
216
+
217
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: float = 30.0):
218
+ if not api_key:
219
+ raise ValueError("Domani: api_key is required")
220
+ self.api_key = api_key
221
+ self.base_url = base_url.rstrip("/")
222
+ self.timeout = timeout
223
+
224
+ def _request(self, method: str, path: str, json: Any = None, query: Optional[Dict[str, Any]] = None) -> Any:
225
+ url = self.base_url + path
226
+ if query:
227
+ pairs = []
228
+ for k, v in query.items():
229
+ if v is None:
230
+ continue
231
+ if isinstance(v, (list, tuple)):
232
+ pairs.extend((k, str(item)) for item in v)
233
+ else:
234
+ pairs.append((k, str(v)))
235
+ if pairs:
236
+ url += ("&" if "?" in url else "?") + urllib.parse.urlencode(pairs)
237
+ headers = {"Authorization": f"Bearer {self.api_key}"}
238
+ data = None
239
+ if json is not None:
240
+ headers["Content-Type"] = "application/json"
241
+ data = _json.dumps(json).encode("utf-8")
242
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
243
+ try:
244
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
245
+ text = resp.read().decode("utf-8")
246
+ return _json.loads(text) if text else None
247
+ except urllib.error.HTTPError as e:
248
+ text = e.read().decode("utf-8")
249
+ body: Any = None
250
+ if text:
251
+ try:
252
+ body = _json.loads(text)
253
+ except ValueError:
254
+ body = text
255
+ raise DomaniError(e.code, body) from None
256
+
257
+ def add_mailbox_alias(self, address: str, body: Dict[str, Any]) -> Any:
258
+ """Add a mailbox alias (POST /api/emails/{address}/aliases)"""
259
+ return self._request("POST", f"/api/emails/{quote(str(address))}/aliases", json=body)
260
+
261
+ def add_suppression(self, body: Dict[str, Any]) -> Any:
262
+ """Add a suppression (POST /api/suppressions)"""
263
+ return self._request("POST", f"/api/suppressions", json=body)
264
+
265
+ def buy_domain(self, body: Dict[str, Any]) -> Any:
266
+ """Purchase one or more domains (POST /api/domains/buy)"""
267
+ return self._request("POST", f"/api/domains/buy", json=body)
268
+
269
+ def cancel_backorder(self, id: str) -> Any:
270
+ """Cancel a backorder (DELETE /api/backorders/{id})"""
271
+ return self._request("DELETE", f"/api/backorders/{quote(str(id))}")
272
+
273
+ def cancel_broker_request(self, id: str) -> Any:
274
+ """Cancel a broker request (DELETE /api/broker/{id})"""
275
+ return self._request("DELETE", f"/api/broker/{quote(str(id))}")
276
+
277
+ def cancel_listing(self, domain: str) -> Any:
278
+ """Cancel a domain listing (DELETE /api/domains/{domain}/for-sale)"""
279
+ return self._request("DELETE", f"/api/domains/{quote(str(domain))}/for-sale")
280
+
281
+ def cancel_negotiation(self, id: str) -> Any:
282
+ """Withdraw a negotiation (DELETE /api/negotiations/{id})"""
283
+ return self._request("DELETE", f"/api/negotiations/{quote(str(id))}")
284
+
285
+ def check_email(self, domain: str) -> Any:
286
+ """Check email DNS health (MX, SPF, DMARC, DKIM) (GET /api/domains/{domain}/email/check)"""
287
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/email/check")
288
+
289
+ def check_transfer_eligibility(self, query: Optional[Dict[str, Any]] = None) -> Any:
290
+ """Pre-check transfer eligibility (GET /api/domains/transfer-check)"""
291
+ return self._request("GET", f"/api/domains/transfer-check", query=query)
292
+
293
+ def check_transfer_status(self, domain: str) -> Any:
294
+ """Check inbound transfer status (GET /api/domains/{domain}/transfer-status)"""
295
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/transfer-status")
296
+
297
+ def clear_catch_all(self, domain: str) -> Any:
298
+ """Clear domain catch-all (DELETE /api/domains/{domain}/email/catch-all)"""
299
+ return self._request("DELETE", f"/api/domains/{quote(str(domain))}/email/catch-all")
300
+
301
+ def clear_domain_redirect(self, domain: str) -> Any:
302
+ """Stop forwarding a domain (DELETE /api/domains/{domain}/redirect)"""
303
+ return self._request("DELETE", f"/api/domains/{quote(str(domain))}/redirect")
304
+
305
+ def connect_domain(self, domain: str, body: Dict[str, Any]) -> Any:
306
+ """Connect a domain to a hosting or email provider (POST /api/domains/{domain}/connect)"""
307
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/connect", json=body)
308
+
309
+ def create_backorder(self, body: Dict[str, Any]) -> Any:
310
+ """Place a backorder on a taken domain (POST /api/backorders)"""
311
+ return self._request("POST", f"/api/backorders", json=body)
312
+
313
+ def create_broker_request(self, body: Dict[str, Any]) -> Any:
314
+ """Request acquisition of a taken domain (POST /api/broker)"""
315
+ return self._request("POST", f"/api/broker", json=body)
316
+
317
+ def create_deal(self, body: Dict[str, Any]) -> Any:
318
+ """Purchase an external domain (create deal) (POST /api/deals)"""
319
+ return self._request("POST", f"/api/deals", json=body)
320
+
321
+ def create_listing(self, domain: str, body: Dict[str, Any]) -> Any:
322
+ """List a domain for sale (PUT /api/domains/{domain}/for-sale)"""
323
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/for-sale", json=body)
324
+
325
+ def create_mailbox(self, body: Dict[str, Any]) -> Any:
326
+ """Create a mailbox on domani.run (POST /api/email)"""
327
+ return self._request("POST", f"/api/email", json=body)
328
+
329
+ def create_mailbox_by_address(self, body: Dict[str, Any] = None) -> Any:
330
+ """Create a mailbox (POST /api/emails)"""
331
+ return self._request("POST", f"/api/emails", json=body)
332
+
333
+ def create_token(self, body: Dict[str, Any]) -> Any:
334
+ """Create a new API token (POST /api/tokens)"""
335
+ return self._request("POST", f"/api/tokens", json=body)
336
+
337
+ def create_webhook(self, body: Dict[str, Any]) -> Any:
338
+ """Create webhook (POST /api/webhooks)"""
339
+ return self._request("POST", f"/api/webhooks", json=body)
340
+
341
+ def delete_account(self) -> Any:
342
+ """Delete account and all associated data (DELETE /api/me)"""
343
+ return self._request("DELETE", f"/api/me")
344
+
345
+ def delete_mailbox_by_address(self, address: str, body: Dict[str, Any] = None) -> Any:
346
+ """Delete a mailbox (DELETE /api/emails/{address})"""
347
+ return self._request("DELETE", f"/api/emails/{quote(str(address))}", json=body)
348
+
349
+ def delete_message_by_address(self, address: str, id: str) -> Any:
350
+ """Delete a message (DELETE /api/emails/{address}/messages/{id})"""
351
+ return self._request("DELETE", f"/api/emails/{quote(str(address))}/messages/{quote(str(id))}")
352
+
353
+ def delete_messages_by_address(self, address: str, body: Dict[str, Any] = None) -> Any:
354
+ """Bulk delete messages (POST /api/emails/{address}/messages/delete)"""
355
+ return self._request("POST", f"/api/emails/{quote(str(address))}/messages/delete", json=body)
356
+
357
+ def delete_webhook(self, id: str) -> Any:
358
+ """Delete webhook (DELETE /api/webhooks/{id})"""
359
+ return self._request("DELETE", f"/api/webhooks/{quote(str(id))}")
360
+
361
+ def dns_check(self, query: Optional[Dict[str, Any]] = None) -> Any:
362
+ """Fast DNS-based domain existence check (GET /api/domains/dns-check)"""
363
+ return self._request("GET", f"/api/domains/dns-check", query=query)
364
+
365
+ def finalize_negotiation(self, id: str, body: Dict[str, Any] = None) -> Any:
366
+ """Finalize an agreed negotiation (pay) (POST /api/negotiations/{id}/finalize)"""
367
+ return self._request("POST", f"/api/negotiations/{quote(str(id))}/finalize", json=body)
368
+
369
+ def forward_message_by_address(self, address: str, id: str, body: Dict[str, Any] = None) -> Any:
370
+ """Forward a message (POST /api/emails/{address}/messages/{id}/forward)"""
371
+ return self._request("POST", f"/api/emails/{quote(str(address))}/messages/{quote(str(id))}/forward", json=body)
372
+
373
+ def get_account(self) -> Any:
374
+ """Get current account details (GET /api/me)"""
375
+ return self._request("GET", f"/api/me")
376
+
377
+ def get_auth_code(self, domain: str) -> Any:
378
+ """Get EPP auth code (GET /api/domains/{domain}/auth-code)"""
379
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/auth-code")
380
+
381
+ def get_backorder(self, id: str) -> Any:
382
+ """Get a backorder (GET /api/backorders/{id})"""
383
+ return self._request("GET", f"/api/backorders/{quote(str(id))}")
384
+
385
+ def get_broker_inquiry_state(self, query: Optional[Dict[str, Any]] = None) -> Any:
386
+ """Read a broker inquiry's state (owner side) (GET /api/broker/respond)"""
387
+ return self._request("GET", f"/api/broker/respond", query=query)
388
+
389
+ def get_broker_request(self, id: str) -> Any:
390
+ """Get a broker request (GET /api/broker/{id})"""
391
+ return self._request("GET", f"/api/broker/{quote(str(id))}")
392
+
393
+ def get_contact(self) -> Any:
394
+ """Get registrant contact info (GET /api/me/contact)"""
395
+ return self._request("GET", f"/api/me/contact")
396
+
397
+ def get_deal(self, id: str) -> Any:
398
+ """Get deal detail with timeline (GET /api/deals/{id})"""
399
+ return self._request("GET", f"/api/deals/{quote(str(id))}")
400
+
401
+ def get_deal_invoice(self, id: str) -> Any:
402
+ """Get a deal receipt / statement (GET /api/deals/{id}/invoice)"""
403
+ return self._request("GET", f"/api/deals/{quote(str(id))}/invoice")
404
+
405
+ def get_dns_records(self, domain: str) -> Any:
406
+ """Get DNS records for a domain you own (GET /api/domains/{domain}/dns)"""
407
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/dns")
408
+
409
+ def get_domain_email_status(self, domain: str) -> Any:
410
+ """Check email DNS status (GET /api/domains/{domain}/email/status)"""
411
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/email/status")
412
+
413
+ def get_domain_info(self, domain: str) -> Any:
414
+ """Get detailed information about a domain you own (GET /api/domains/{domain})"""
415
+ return self._request("GET", f"/api/domains/{quote(str(domain))}")
416
+
417
+ def get_domain_og(self, domain: str) -> Any:
418
+ """Get website preview metadata for a domain (GET /api/domains/{domain}/og)"""
419
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/og")
420
+
421
+ def get_domain_status(self, domain: str) -> Any:
422
+ """Check domain health: DNS, SSL, email, expiry (GET /api/domains/{domain}/status)"""
423
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/status")
424
+
425
+ def get_mailbox_avatar_by_address(self, address: str) -> Any:
426
+ """Get mailbox avatar info (GET /api/emails/{address}/avatar)"""
427
+ return self._request("GET", f"/api/emails/{quote(str(address))}/avatar")
428
+
429
+ def get_mailbox_by_address(self, address: str) -> Any:
430
+ """Get mailbox details (GET /api/emails/{address})"""
431
+ return self._request("GET", f"/api/emails/{quote(str(address))}")
432
+
433
+ def get_mailbox_webhook(self, address: str) -> Any:
434
+ """Get mailbox webhook config (GET /api/emails/{address}/webhook)"""
435
+ return self._request("GET", f"/api/emails/{quote(str(address))}/webhook")
436
+
437
+ def get_message_by_address(self, address: str, id: str) -> Any:
438
+ """Get a message (GET /api/emails/{address}/messages/{id})"""
439
+ return self._request("GET", f"/api/emails/{quote(str(address))}/messages/{quote(str(id))}")
440
+
441
+ def get_nameservers(self, domain: str) -> Any:
442
+ """Get nameservers for a domain (GET /api/domains/{domain}/nameservers)"""
443
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/nameservers")
444
+
445
+ def get_negotiation(self, id: str) -> Any:
446
+ """Get a negotiation thread (GET /api/negotiations/{id})"""
447
+ return self._request("GET", f"/api/negotiations/{quote(str(id))}")
448
+
449
+ def get_notification_preferences(self) -> Any:
450
+ """Get notification preferences (GET /api/notifications/preferences)"""
451
+ return self._request("GET", f"/api/notifications/preferences")
452
+
453
+ def get_parking_analytics(self, domain: str) -> Any:
454
+ """Get parking analytics (GET /api/domains/{domain}/analytics)"""
455
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/analytics")
456
+
457
+ def get_payout_status(self) -> Any:
458
+ """Get marketplace payout status (GET /api/billing/connect)"""
459
+ return self._request("GET", f"/api/billing/connect")
460
+
461
+ def get_referrals(self) -> Any:
462
+ """Get referral earnings and history (GET /api/referrals)"""
463
+ return self._request("GET", f"/api/referrals")
464
+
465
+ def get_sell_status(self, query: Optional[Dict[str, Any]] = None) -> Any:
466
+ """Check listing and deal status (GET /api/domains/sell/status)"""
467
+ return self._request("GET", f"/api/domains/sell/status", query=query)
468
+
469
+ def get_transfer_away(self, domain: str) -> Any:
470
+ """Get outbound transfer status (GET /api/domains/{domain}/transfer-away)"""
471
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/transfer-away")
472
+
473
+ def get_unread_count(self) -> Any:
474
+ """Get unread notification count (GET /api/notifications/count)"""
475
+ return self._request("GET", f"/api/notifications/count")
476
+
477
+ def get_webhook_deliveries(self, id: str, query: Optional[Dict[str, Any]] = None) -> Any:
478
+ """List webhook deliveries (GET /api/webhooks/{id}/deliveries)"""
479
+ return self._request("GET", f"/api/webhooks/{quote(str(id))}/deliveries", query=query)
480
+
481
+ def import_domain(self, body: Dict[str, Any]) -> Any:
482
+ """Import an external domain (POST /api/domains/import)"""
483
+ return self._request("POST", f"/api/domains/import", json=body)
484
+
485
+ def list_backorders(self, query: Optional[Dict[str, Any]] = None) -> Any:
486
+ """List your backorders (GET /api/backorders)"""
487
+ return self._request("GET", f"/api/backorders", query=query)
488
+
489
+ def list_broker_requests(self, query: Optional[Dict[str, Any]] = None) -> Any:
490
+ """List your broker requests (GET /api/broker)"""
491
+ return self._request("GET", f"/api/broker", query=query)
492
+
493
+ def list_connect_providers(self, domain: str) -> Any:
494
+ """List available providers for connecting a domain (GET /api/domains/{domain}/connect)"""
495
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/connect")
496
+
497
+ def list_deals(self, query: Optional[Dict[str, Any]] = None) -> Any:
498
+ """List your deals (GET /api/deals)"""
499
+ return self._request("GET", f"/api/deals", query=query)
500
+
501
+ def list_domains(self) -> Any:
502
+ """List all your registered domains (GET /api/domains)"""
503
+ return self._request("GET", f"/api/domains")
504
+
505
+ def list_email_messages_by_address(self, address: str, query: Optional[Dict[str, Any]] = None) -> Any:
506
+ """List emails (GET /api/emails/{address}/messages)"""
507
+ return self._request("GET", f"/api/emails/{quote(str(address))}/messages", query=query)
508
+
509
+ def list_invoices(self, query: Optional[Dict[str, Any]] = None) -> Any:
510
+ """List payment invoices (GET /api/billing/invoices)"""
511
+ return self._request("GET", f"/api/billing/invoices", query=query)
512
+
513
+ def list_mailbox_aliases(self, address: str) -> Any:
514
+ """List mailbox aliases (GET /api/emails/{address}/aliases)"""
515
+ return self._request("GET", f"/api/emails/{quote(str(address))}/aliases")
516
+
517
+ def list_mailboxes(self, query: Optional[Dict[str, Any]] = None) -> Any:
518
+ """List all mailboxes (GET /api/email)"""
519
+ return self._request("GET", f"/api/email", query=query)
520
+
521
+ def list_mailboxes_by_address(self, query: Optional[Dict[str, Any]] = None) -> Any:
522
+ """List all mailboxes (GET /api/emails)"""
523
+ return self._request("GET", f"/api/emails", query=query)
524
+
525
+ def list_negotiations(self, query: Optional[Dict[str, Any]] = None) -> Any:
526
+ """List your negotiations (GET /api/negotiations)"""
527
+ return self._request("GET", f"/api/negotiations", query=query)
528
+
529
+ def list_notifications(self, query: Optional[Dict[str, Any]] = None) -> Any:
530
+ """List notifications (GET /api/notifications)"""
531
+ return self._request("GET", f"/api/notifications", query=query)
532
+
533
+ def list_suppressions(self, query: Optional[Dict[str, Any]] = None) -> Any:
534
+ """List suppressed addresses (GET /api/suppressions)"""
535
+ return self._request("GET", f"/api/suppressions", query=query)
536
+
537
+ def list_tlds(self, query: Optional[Dict[str, Any]] = None) -> Any:
538
+ """List all available TLDs with pricing (GET /api/tlds)"""
539
+ return self._request("GET", f"/api/tlds", query=query)
540
+
541
+ def list_tokens(self) -> Any:
542
+ """List your API tokens (GET /api/tokens)"""
543
+ return self._request("GET", f"/api/tokens")
544
+
545
+ def list_verify_services(self, domain: str) -> Any:
546
+ """List supported services for domain verification (GET /api/domains/{domain}/verify-service)"""
547
+ return self._request("GET", f"/api/domains/{quote(str(domain))}/verify-service")
548
+
549
+ def list_webhook_events(self) -> Any:
550
+ """List webhook event types (GET /api/webhooks/events)"""
551
+ return self._request("GET", f"/api/webhooks/events")
552
+
553
+ def list_webhooks(self) -> Any:
554
+ """List webhooks (GET /api/webhooks)"""
555
+ return self._request("GET", f"/api/webhooks")
556
+
557
+ def login_user(self, body: Dict[str, Any]) -> Any:
558
+ """Send a magic link sign-in email (POST /api/auth/login)"""
559
+ return self._request("POST", f"/api/auth/login", json=body)
560
+
561
+ def make_offer(self, body: Dict[str, Any]) -> Any:
562
+ """Make an offer on a listing (POST /api/negotiations)"""
563
+ return self._request("POST", f"/api/negotiations", json=body)
564
+
565
+ def mark_all_notifications_read(self) -> Any:
566
+ """Mark all notifications as read (POST /api/notifications/read-all)"""
567
+ return self._request("POST", f"/api/notifications/read-all")
568
+
569
+ def mark_messages_read_by_address(self, address: str, body: Dict[str, Any] = None) -> Any:
570
+ """Mark messages as read or unread (PATCH /api/emails/{address}/messages/read)"""
571
+ return self._request("PATCH", f"/api/emails/{quote(str(address))}/messages/read", json=body)
572
+
573
+ def mark_notification_read(self, id: str, body: Dict[str, Any]) -> Any:
574
+ """Mark notification as read (PATCH /api/notifications/{id})"""
575
+ return self._request("PATCH", f"/api/notifications/{quote(str(id))}", json=body)
576
+
577
+ def provide_epp_code(self, id: str, body: Dict[str, Any]) -> Any:
578
+ """Provide EPP/auth code (seller) (PATCH /api/deals/{id})"""
579
+ return self._request("PATCH", f"/api/deals/{quote(str(id))}", json=body)
580
+
581
+ def provision_agent(self, body: Dict[str, Any]) -> Any:
582
+ """Provision a complete agent identity in one call (POST /api/agents/provision)"""
583
+ return self._request("POST", f"/api/agents/provision", json=body)
584
+
585
+ def register_user(self, body: Dict[str, Any]) -> Any:
586
+ """Create a new account (POST /api/auth/register)"""
587
+ return self._request("POST", f"/api/auth/register", json=body)
588
+
589
+ def remove_card(self) -> Any:
590
+ """Remove saved payment method (DELETE /api/billing/card)"""
591
+ return self._request("DELETE", f"/api/billing/card")
592
+
593
+ def remove_domain_email(self, domain: str) -> Any:
594
+ """Remove email from a domain (DELETE /api/domains/{domain}/email/setup)"""
595
+ return self._request("DELETE", f"/api/domains/{quote(str(domain))}/email/setup")
596
+
597
+ def remove_mailbox_alias(self, address: str, alias: str) -> Any:
598
+ """Remove a mailbox alias (DELETE /api/emails/{address}/aliases/{alias})"""
599
+ return self._request("DELETE", f"/api/emails/{quote(str(address))}/aliases/{quote(str(alias))}")
600
+
601
+ def remove_mailbox_webhook(self, address: str) -> Any:
602
+ """Remove mailbox webhook (DELETE /api/emails/{address}/webhook)"""
603
+ return self._request("DELETE", f"/api/emails/{quote(str(address))}/webhook")
604
+
605
+ def remove_suppression(self, address: str) -> Any:
606
+ """Remove a suppression (DELETE /api/suppressions/{address})"""
607
+ return self._request("DELETE", f"/api/suppressions/{quote(str(address))}")
608
+
609
+ def renew_domain(self, domain: str, body: Dict[str, Any] = None) -> Any:
610
+ """Renew a domain (POST /api/domains/{domain}/renew)"""
611
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/renew", json=body)
612
+
613
+ def reply_to_message_by_address(self, address: str, id: str, body: Dict[str, Any] = None) -> Any:
614
+ """Reply to a message (POST /api/emails/{address}/messages/{id}/reply)"""
615
+ return self._request("POST", f"/api/emails/{quote(str(address))}/messages/{quote(str(id))}/reply", json=body)
616
+
617
+ def resend_email_verification(self, body: Dict[str, Any] = None) -> Any:
618
+ """Resend contact email verification (POST /api/me/resend-verification)"""
619
+ return self._request("POST", f"/api/me/resend-verification", json=body)
620
+
621
+ def respond_to_broker_inquiry(self, body: Dict[str, Any]) -> Any:
622
+ """Respond to a broker inquiry (owner side) (POST /api/broker/respond)"""
623
+ return self._request("POST", f"/api/broker/respond", json=body)
624
+
625
+ def respond_to_negotiation(self, id: str, body: Dict[str, Any]) -> Any:
626
+ """Counter, accept, or decline (POST /api/negotiations/{id})"""
627
+ return self._request("POST", f"/api/negotiations/{quote(str(id))}", json=body)
628
+
629
+ def restore_dns(self, domain: str, body: Dict[str, Any] = None) -> Any:
630
+ """Restore DNS from a snapshot (POST /api/domains/{domain}/dns/restore)"""
631
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/dns/restore", json=body)
632
+
633
+ def revoke_token(self, id: str) -> Any:
634
+ """Revoke an API token (DELETE /api/tokens/{id})"""
635
+ return self._request("DELETE", f"/api/tokens/{quote(str(id))}")
636
+
637
+ def rotate_mailbox_webhook_secret(self, address: str) -> Any:
638
+ """Rotate webhook signing secret (POST /api/emails/{address}/webhook/rotate)"""
639
+ return self._request("POST", f"/api/emails/{quote(str(address))}/webhook/rotate")
640
+
641
+ def search(self, query: Optional[Dict[str, Any]] = None) -> Any:
642
+ """Check availability and pricing for domains (GET /api/domains/search)"""
643
+ return self._request("GET", f"/api/domains/search", query=query)
644
+
645
+ def sell_domain(self, body: Dict[str, Any]) -> Any:
646
+ """List a domain for sale (open marketplace) (POST /api/domains/sell)"""
647
+ return self._request("POST", f"/api/domains/sell", json=body)
648
+
649
+ def send_email_by_address(self, address: str, body: Dict[str, Any] = None) -> Any:
650
+ """Send an email (POST /api/emails/{address}/send)"""
651
+ return self._request("POST", f"/api/emails/{quote(str(address))}/send", json=body)
652
+
653
+ def set_catch_all(self, domain: str, body: Dict[str, Any]) -> Any:
654
+ """Set domain catch-all (PUT /api/domains/{domain}/email/catch-all)"""
655
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/email/catch-all", json=body)
656
+
657
+ def set_contact(self, body: "RegistrantContact") -> Any:
658
+ """Set registrant contact info (PUT /api/me/contact)"""
659
+ return self._request("PUT", f"/api/me/contact", json=body)
660
+
661
+ def set_dns_records(self, domain: str, body: Dict[str, Any]) -> Any:
662
+ """Set DNS records for a domain you own (PUT /api/domains/{domain}/dns)"""
663
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/dns", json=body)
664
+
665
+ def set_domain_redirect(self, domain: str, body: Dict[str, Any]) -> Any:
666
+ """Forward a domain to a URL (PUT /api/domains/{domain}/redirect)"""
667
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/redirect", json=body)
668
+
669
+ def set_mailbox_webhook(self, address: str, body: Dict[str, Any]) -> Any:
670
+ """Set mailbox webhook (PUT /api/emails/{address}/webhook)"""
671
+ return self._request("PUT", f"/api/emails/{quote(str(address))}/webhook", json=body)
672
+
673
+ def set_nameservers(self, domain: str, body: Dict[str, Any]) -> Any:
674
+ """Set nameservers for a domain (PUT /api/domains/{domain}/nameservers)"""
675
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/nameservers", json=body)
676
+
677
+ def setup_billing(self, body: Dict[str, Any] = None) -> Any:
678
+ """Get checkout URL for adding a payment method (POST /api/billing/setup)"""
679
+ return self._request("POST", f"/api/billing/setup", json=body)
680
+
681
+ def setup_domain_email(self, domain: str, body: Dict[str, Any] = None) -> Any:
682
+ """Set up email on a domain (POST /api/domains/{domain}/email/setup)"""
683
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/email/setup", json=body)
684
+
685
+ def snapshot_dns(self, domain: str, body: Dict[str, Any] = None) -> Any:
686
+ """Capture a DNS snapshot (POST /api/domains/{domain}/dns/snapshot)"""
687
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/dns/snapshot", json=body)
688
+
689
+ def start_payout_onboarding(self) -> Any:
690
+ """Start bank payout onboarding (POST /api/billing/connect)"""
691
+ return self._request("POST", f"/api/billing/connect")
692
+
693
+ def suggest_domains(self, query: Optional[Dict[str, Any]] = None) -> Any:
694
+ """AI-powered domain suggestions with availability (GET /api/domains/suggest)"""
695
+ return self._request("GET", f"/api/domains/suggest", query=query)
696
+
697
+ def test_mailbox_webhook(self, address: str) -> Any:
698
+ """Test mailbox webhook (POST /api/emails/{address}/webhook/test)"""
699
+ return self._request("POST", f"/api/emails/{quote(str(address))}/webhook/test")
700
+
701
+ def transfer_domain(self, body: Dict[str, Any]) -> Any:
702
+ """Transfer domain from another provider (POST /api/domains/transfer)"""
703
+ return self._request("POST", f"/api/domains/transfer", json=body)
704
+
705
+ def update_account(self, body: Dict[str, Any]) -> Any:
706
+ """Update account preferences (PUT /api/me)"""
707
+ return self._request("PUT", f"/api/me", json=body)
708
+
709
+ def update_domain_settings(self, domain: str, body: Dict[str, Any]) -> Any:
710
+ """Update domain settings (PUT /api/domains/{domain}/settings)"""
711
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/settings", json=body)
712
+
713
+ def update_listing(self, domain: str, body: Dict[str, Any]) -> Any:
714
+ """Update a domain listing (PATCH /api/domains/{domain}/for-sale)"""
715
+ return self._request("PATCH", f"/api/domains/{quote(str(domain))}/for-sale", json=body)
716
+
717
+ def update_mailbox_by_address(self, address: str, body: Dict[str, Any] = None) -> Any:
718
+ """Update mailbox (PATCH /api/emails/{address})"""
719
+ return self._request("PATCH", f"/api/emails/{quote(str(address))}", json=body)
720
+
721
+ def update_notification_preferences(self, body: Dict[str, Any]) -> Any:
722
+ """Update notification preferences (PUT /api/notifications/preferences)"""
723
+ return self._request("PUT", f"/api/notifications/preferences", json=body)
724
+
725
+ def update_parking(self, domain: str, body: Dict[str, Any]) -> Any:
726
+ """Update parking settings (PUT /api/domains/{domain}/parking)"""
727
+ return self._request("PUT", f"/api/domains/{quote(str(domain))}/parking", json=body)
728
+
729
+ def update_webhook(self, id: str, body: Dict[str, Any]) -> Any:
730
+ """Update webhook (PATCH /api/webhooks/{id})"""
731
+ return self._request("PATCH", f"/api/webhooks/{quote(str(id))}", json=body)
732
+
733
+ def verify_connection(self, domain: str, body: Dict[str, Any]) -> Any:
734
+ """Verify that a provider connection is working (POST /api/domains/{domain}/verify)"""
735
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/verify", json=body)
736
+
737
+ def verify_import(self, body: Dict[str, Any]) -> Any:
738
+ """Verify and complete domain import (POST /api/domains/import/verify)"""
739
+ return self._request("POST", f"/api/domains/import/verify", json=body)
740
+
741
+ def verify_magic_link(self, query: Optional[Dict[str, Any]] = None) -> Any:
742
+ """Verify a magic link token (GET /api/auth/verify)"""
743
+ return self._request("GET", f"/api/auth/verify", query=query)
744
+
745
+ def verify_sell_listing(self, body: Dict[str, Any]) -> Any:
746
+ """Verify TXT ownership for external listing (POST /api/domains/sell/verify)"""
747
+ return self._request("POST", f"/api/domains/sell/verify", json=body)
748
+
749
+ def verify_service(self, domain: str, body: Dict[str, Any]) -> Any:
750
+ """Add DNS records to verify domain ownership for a third-party service (POST /api/domains/{domain}/verify-service)"""
751
+ return self._request("POST", f"/api/domains/{quote(str(domain))}/verify-service", json=body)
752
+
753
+ def watch_transfer(self, body: Dict[str, Any]) -> Any:
754
+ """Watch a domain for transfer eligibility (POST /api/domains/transfer-watch)"""
755
+ return self._request("POST", f"/api/domains/transfer-watch", json=body)
756
+
757
+ def whois_lookup(self, query: Optional[Dict[str, Any]] = None) -> Any:
758
+ """Look up domain registration data via RDAP (GET /api/domains/whois)"""
759
+ return self._request("GET", f"/api/domains/whois", query=query)
760
+
761
+ def withdraw_balance(self) -> Any:
762
+ """Withdraw marketplace balance to bank (POST /api/billing/connect/payout)"""
763
+ return self._request("POST", f"/api/billing/connect/payout")
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """End-to-end check for the domani Python SDK against a running server.
3
+
4
+ Reads DOMANI_API_KEY, DOMANI_BASE_URL, DOMANI_DOMAIN from the environment,
5
+ runs the README quickstart flow, and exits 0 on success / 1 on any failure.
6
+ Invoked by the `sdk.python_e2e` eval scenario; also runnable by hand against a
7
+ booted mock-mode server.
8
+ """
9
+ import os
10
+ import sys
11
+
12
+ # Import the sibling package without installing it.
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+ from domani import Domani, DomaniError # noqa: E402
15
+
16
+
17
+ def fail(msg):
18
+ print(f"[sdk-py-e2e] FAIL: {msg}", file=sys.stderr)
19
+ sys.exit(1)
20
+
21
+
22
+ def main():
23
+ api_key = os.environ.get("DOMANI_API_KEY")
24
+ base_url = os.environ.get("DOMANI_BASE_URL")
25
+ domain = os.environ.get("DOMANI_DOMAIN")
26
+ if not (api_key and base_url and domain):
27
+ fail("DOMANI_API_KEY, DOMANI_BASE_URL, DOMANI_DOMAIN must be set")
28
+
29
+ domani = Domani(api_key=api_key, base_url=base_url)
30
+ address = f"hi@{domain}"
31
+
32
+ # Typed search (README shape: search(query={...}))
33
+ results = domani.search(query={"q": f"sdk-py-{domain}"})
34
+ if not isinstance(results, dict):
35
+ fail(f"search returned non-dict: {results!r}")
36
+
37
+ # Create a mailbox (README shape)
38
+ mb = domani.create_mailbox_by_address(body={"address": address})
39
+ if not isinstance(mb, dict) or mb.get("address") != address:
40
+ fail(f"create_mailbox address mismatch: {mb!r}")
41
+
42
+ # Send (README shape)
43
+ sent = domani.send_email_by_address(
44
+ address, body={"to": "user@example.com", "subject": "SDK E2E", "text": "hi from the Python SDK"}
45
+ )
46
+ if not isinstance(sent, dict) or not sent.get("status"):
47
+ fail(f"send returned no status: {sent!r}")
48
+
49
+ # Suppression: add -> list -> a blocked send raises DomaniError -> remove
50
+ domani.add_suppression(body={"address": "blocked@example.com"})
51
+ listed = domani.list_suppressions()
52
+ addrs = [s.get("address") for s in (listed or {}).get("suppressions", [])]
53
+ if "blocked@example.com" not in addrs:
54
+ fail(f"suppression not listed: {listed!r}")
55
+
56
+ raised = False
57
+ try:
58
+ domani.send_email_by_address(address, body={"to": "blocked@example.com", "text": "should be blocked"})
59
+ except DomaniError as e:
60
+ if e.status == 400 and e.code == "RECIPIENTS_SUPPRESSED":
61
+ raised = True
62
+ else:
63
+ fail(f"wrong DomaniError: {e.status} {e.code}")
64
+ if not raised:
65
+ fail("blocked send should have raised DomaniError")
66
+
67
+ domani.remove_suppression("blocked@example.com")
68
+ print("[sdk-py-e2e] OK")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "domani"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the domani API - domains, email, and identity for AI agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["domani", "domains", "email", "api", "sdk", "agents", "mcp"]
13
+ authors = [{ name = "domani" }]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Intended Audience :: Developers",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://domani.run"
22
+ Repository = "https://github.com/gwendall/domani"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["domani"]