chatads-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.
- chatads_sdk-0.1.0/PKG-INFO +74 -0
- chatads_sdk-0.1.0/README.md +63 -0
- chatads_sdk-0.1.0/chatads_sdk/__init__.py +27 -0
- chatads_sdk-0.1.0/chatads_sdk/client.py +385 -0
- chatads_sdk-0.1.0/chatads_sdk/exceptions.py +49 -0
- chatads_sdk-0.1.0/chatads_sdk/models.py +227 -0
- chatads_sdk-0.1.0/chatads_sdk.egg-info/PKG-INFO +74 -0
- chatads_sdk-0.1.0/chatads_sdk.egg-info/SOURCES.txt +11 -0
- chatads_sdk-0.1.0/chatads_sdk.egg-info/dependency_links.txt +1 -0
- chatads_sdk-0.1.0/chatads_sdk.egg-info/requires.txt +4 -0
- chatads_sdk-0.1.0/chatads_sdk.egg-info/top_level.txt +1 -0
- chatads_sdk-0.1.0/pyproject.toml +17 -0
- chatads_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatads-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Python client for the ChatAds affiliate scoring API
|
|
5
|
+
Author-email: ChatAds <support@getchatads.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
9
|
+
Provides-Extra: async
|
|
10
|
+
Requires-Dist: httpx[http2]<1.0,>=0.27; extra == "async"
|
|
11
|
+
|
|
12
|
+
# ChatAds Python SDK
|
|
13
|
+
|
|
14
|
+
A tiny, dependency-light wrapper around the ChatAds `/v1/chatads-script` endpoint. It mirrors the response payloads returned by the FastAPI service so you can drop it into CLIs, serverless functions, or orchestration tools.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
(Or build/upload to your internal index as needed.)
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from chatads_sdk import ChatAdsClient, FunctionItemPayload
|
|
28
|
+
|
|
29
|
+
client = ChatAdsClient(
|
|
30
|
+
api_key="YOUR_X_API_KEY",
|
|
31
|
+
base_url="https://<your-chatads-domain>",
|
|
32
|
+
raise_on_failure=True, # Treat success=False payloads as exceptions
|
|
33
|
+
max_retries=2, # Optional automatic retries for 429/5xx responses
|
|
34
|
+
retry_backoff_factor=0.75, # Exponential backoff multiplier
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
payload = FunctionItemPayload(
|
|
38
|
+
message="Looking for a CRM to close more deals",
|
|
39
|
+
ip="1.2.3.4",
|
|
40
|
+
user_agent="Mozilla/5.0",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = client.analyze(payload)
|
|
44
|
+
|
|
45
|
+
if result.success:
|
|
46
|
+
print(result.data.ad)
|
|
47
|
+
else:
|
|
48
|
+
print(result.error.code, result.error.message)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Error Handling
|
|
52
|
+
|
|
53
|
+
Non-2xx responses raise `ChatAdsAPIError` and include the parsed error payload plus the original HTTP status code so you can branch on quota/validation failures. Set `raise_on_failure=True` if you want 200 responses with `success=false` to raise the same exception class.
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- Retries are opt-in. Provide `max_retries>0` to automatically retry transport errors and retryable status codes. The client honors `Retry-After` headers and falls back to exponential backoff.
|
|
58
|
+
- `FunctionItemPayload` matches the server-side `FunctionItem` pydantic model. Keyword arguments passed to `ChatAdsClient.analyze_message()` accept either snake_case (`user_agent`) or camelCase (`userAgent`) keys.
|
|
59
|
+
- Reserved payload keys (e.g., `message`, `pageUrl`, `userAgent`) cannot be overridden through `extra_fields`; doing so raises `ValueError` to prevent silent mutations.
|
|
60
|
+
|
|
61
|
+
## CLI Smoke Test
|
|
62
|
+
|
|
63
|
+
For a super-quick check, edit the config block at the top of `python_sdk/run_sdk_smoke.py` (or set the
|
|
64
|
+
`CHATADS_*` env vars) and run:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
PYTHONPATH=python_sdk python python_sdk/run_sdk_smoke.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It prints the raw JSON response or surfaces a `ChatAdsAPIError` with status/error fields so you can see
|
|
71
|
+
exactly what the API returned.
|
|
72
|
+
|
|
73
|
+
- `API_KEY` and `MESSAGE` are the only required values. Leave `CALLER_IP`, `USER_AGENT`, or `CHATADS_EXTRA_FIELDS`
|
|
74
|
+
blank to omit them from the request.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ChatAds Python SDK
|
|
2
|
+
|
|
3
|
+
A tiny, dependency-light wrapper around the ChatAds `/v1/chatads-script` endpoint. It mirrors the response payloads returned by the FastAPI service so you can drop it into CLIs, serverless functions, or orchestration tools.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
(Or build/upload to your internal index as needed.)
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from chatads_sdk import ChatAdsClient, FunctionItemPayload
|
|
17
|
+
|
|
18
|
+
client = ChatAdsClient(
|
|
19
|
+
api_key="YOUR_X_API_KEY",
|
|
20
|
+
base_url="https://<your-chatads-domain>",
|
|
21
|
+
raise_on_failure=True, # Treat success=False payloads as exceptions
|
|
22
|
+
max_retries=2, # Optional automatic retries for 429/5xx responses
|
|
23
|
+
retry_backoff_factor=0.75, # Exponential backoff multiplier
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
payload = FunctionItemPayload(
|
|
27
|
+
message="Looking for a CRM to close more deals",
|
|
28
|
+
ip="1.2.3.4",
|
|
29
|
+
user_agent="Mozilla/5.0",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
result = client.analyze(payload)
|
|
33
|
+
|
|
34
|
+
if result.success:
|
|
35
|
+
print(result.data.ad)
|
|
36
|
+
else:
|
|
37
|
+
print(result.error.code, result.error.message)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Error Handling
|
|
41
|
+
|
|
42
|
+
Non-2xx responses raise `ChatAdsAPIError` and include the parsed error payload plus the original HTTP status code so you can branch on quota/validation failures. Set `raise_on_failure=True` if you want 200 responses with `success=false` to raise the same exception class.
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
- Retries are opt-in. Provide `max_retries>0` to automatically retry transport errors and retryable status codes. The client honors `Retry-After` headers and falls back to exponential backoff.
|
|
47
|
+
- `FunctionItemPayload` matches the server-side `FunctionItem` pydantic model. Keyword arguments passed to `ChatAdsClient.analyze_message()` accept either snake_case (`user_agent`) or camelCase (`userAgent`) keys.
|
|
48
|
+
- Reserved payload keys (e.g., `message`, `pageUrl`, `userAgent`) cannot be overridden through `extra_fields`; doing so raises `ValueError` to prevent silent mutations.
|
|
49
|
+
|
|
50
|
+
## CLI Smoke Test
|
|
51
|
+
|
|
52
|
+
For a super-quick check, edit the config block at the top of `python_sdk/run_sdk_smoke.py` (or set the
|
|
53
|
+
`CHATADS_*` env vars) and run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
PYTHONPATH=python_sdk python python_sdk/run_sdk_smoke.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It prints the raw JSON response or surfaces a `ChatAdsAPIError` with status/error fields so you can see
|
|
60
|
+
exactly what the API returned.
|
|
61
|
+
|
|
62
|
+
- `API_KEY` and `MESSAGE` are the only required values. Leave `CALLER_IP`, `USER_AGENT`, or `CHATADS_EXTRA_FIELDS`
|
|
63
|
+
blank to omit them from the request.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Public exports for the ChatAds Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .client import ChatAdsClient, AsyncChatAdsClient
|
|
4
|
+
from .models import (
|
|
5
|
+
ChatAdsAd,
|
|
6
|
+
ChatAdsData,
|
|
7
|
+
ChatAdsError,
|
|
8
|
+
ChatAdsMeta,
|
|
9
|
+
ChatAdsResponse,
|
|
10
|
+
FunctionItemPayload,
|
|
11
|
+
UsageInfo,
|
|
12
|
+
)
|
|
13
|
+
from .exceptions import ChatAdsAPIError, ChatAdsSDKError
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ChatAdsClient",
|
|
17
|
+
"AsyncChatAdsClient",
|
|
18
|
+
"ChatAdsAd",
|
|
19
|
+
"ChatAdsData",
|
|
20
|
+
"ChatAdsError",
|
|
21
|
+
"ChatAdsMeta",
|
|
22
|
+
"ChatAdsResponse",
|
|
23
|
+
"FunctionItemPayload",
|
|
24
|
+
"UsageInfo",
|
|
25
|
+
"ChatAdsAPIError",
|
|
26
|
+
"ChatAdsSDKError",
|
|
27
|
+
]
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""HTTP clients for interacting with the ChatAds API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from email.utils import parsedate_to_datetime
|
|
11
|
+
from typing import Any, Dict, Iterable, Optional, Set
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .exceptions import ChatAdsAPIError, ChatAdsSDKError
|
|
16
|
+
from .models import (
|
|
17
|
+
ChatAdsResponse,
|
|
18
|
+
FunctionItemPayload,
|
|
19
|
+
FUNCTION_ITEM_FIELD_ALIASES,
|
|
20
|
+
FUNCTION_ITEM_OPTIONAL_FIELDS,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_DEFAULT_ENDPOINT = "/chatads-script"
|
|
24
|
+
_DEFAULT_RETRY_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
25
|
+
_FUNCTION_ITEM_OPTIONAL_FIELDS = set(FUNCTION_ITEM_OPTIONAL_FIELDS)
|
|
26
|
+
_FIELD_ALIAS_LOOKUP = {alias.lower(): field for alias, field in FUNCTION_ITEM_FIELD_ALIASES.items()}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChatAdsClient:
|
|
30
|
+
"""Synchronous ChatAds API client."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
api_key: str,
|
|
35
|
+
base_url: str,
|
|
36
|
+
*,
|
|
37
|
+
endpoint: str = _DEFAULT_ENDPOINT,
|
|
38
|
+
timeout: float = 10.0,
|
|
39
|
+
http_client: Optional[httpx.Client] = None,
|
|
40
|
+
raise_on_failure: bool = False,
|
|
41
|
+
max_retries: int = 0,
|
|
42
|
+
retry_backoff_factor: float = 0.5,
|
|
43
|
+
retry_statuses: Optional[Iterable[int]] = None,
|
|
44
|
+
logger: Optional[logging.Logger] = None,
|
|
45
|
+
debug: bool = False,
|
|
46
|
+
) -> None:
|
|
47
|
+
if not api_key:
|
|
48
|
+
raise ValueError("api_key is required")
|
|
49
|
+
if not base_url:
|
|
50
|
+
raise ValueError("base_url is required")
|
|
51
|
+
|
|
52
|
+
self._api_key = api_key
|
|
53
|
+
self._base_url = base_url.rstrip("/")
|
|
54
|
+
self._endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
55
|
+
self._timeout = timeout
|
|
56
|
+
self._client = http_client or httpx.Client(timeout=timeout)
|
|
57
|
+
self._owns_client = http_client is None
|
|
58
|
+
self._raise_on_failure = raise_on_failure
|
|
59
|
+
self._max_retries = max(0, int(max_retries))
|
|
60
|
+
self._retry_backoff_factor = max(0.0, float(retry_backoff_factor))
|
|
61
|
+
self._retry_statuses: Set[int] = (
|
|
62
|
+
set(retry_statuses) if retry_statuses is not None else set(_DEFAULT_RETRY_STATUSES)
|
|
63
|
+
)
|
|
64
|
+
self._logger = logger or logging.getLogger("chatads_sdk")
|
|
65
|
+
self._debug = debug
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
if self._owns_client:
|
|
69
|
+
self._client.close()
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> "ChatAdsClient":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
def analyze(
|
|
78
|
+
self,
|
|
79
|
+
payload: FunctionItemPayload,
|
|
80
|
+
*,
|
|
81
|
+
timeout: Optional[float] = None,
|
|
82
|
+
headers: Optional[Dict[str, str]] = None,
|
|
83
|
+
) -> ChatAdsResponse:
|
|
84
|
+
"""Send a FunctionItem payload to the ChatAds endpoint."""
|
|
85
|
+
body = payload.to_payload()
|
|
86
|
+
return self._post(body, timeout=timeout, headers=headers)
|
|
87
|
+
|
|
88
|
+
def analyze_message(
|
|
89
|
+
self,
|
|
90
|
+
message: str,
|
|
91
|
+
*,
|
|
92
|
+
timeout: Optional[float] = None,
|
|
93
|
+
headers: Optional[Dict[str, str]] = None,
|
|
94
|
+
**extra_fields: Any,
|
|
95
|
+
) -> ChatAdsResponse:
|
|
96
|
+
"""
|
|
97
|
+
Convenience wrapper taking only the message plus optional FunctionItem fields.
|
|
98
|
+
"""
|
|
99
|
+
payload = _build_payload_from_kwargs(message, extra_fields)
|
|
100
|
+
return self.analyze(payload, timeout=timeout, headers=headers)
|
|
101
|
+
|
|
102
|
+
def _post(
|
|
103
|
+
self,
|
|
104
|
+
body: Dict[str, Any],
|
|
105
|
+
*,
|
|
106
|
+
timeout: Optional[float],
|
|
107
|
+
headers: Optional[Dict[str, str]],
|
|
108
|
+
) -> ChatAdsResponse:
|
|
109
|
+
request_headers = {"x-api-key": self._api_key, **(headers or {})}
|
|
110
|
+
url = f"{self._base_url}{self._endpoint}"
|
|
111
|
+
attempt = 0
|
|
112
|
+
while True:
|
|
113
|
+
try:
|
|
114
|
+
self._log_request(url, request_headers, body)
|
|
115
|
+
response = self._client.post(
|
|
116
|
+
url,
|
|
117
|
+
json=body,
|
|
118
|
+
headers=request_headers,
|
|
119
|
+
timeout=timeout or self._timeout,
|
|
120
|
+
)
|
|
121
|
+
except httpx.RequestError as exc:
|
|
122
|
+
if attempt >= self._max_retries:
|
|
123
|
+
raise ChatAdsSDKError(f"Transport error while calling ChatAds: {exc}") from exc
|
|
124
|
+
_sleep_sync(
|
|
125
|
+
_compute_retry_delay(attempt, self._retry_backoff_factor, None)
|
|
126
|
+
)
|
|
127
|
+
attempt += 1
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
parsed = _parse_response(response)
|
|
131
|
+
self._log_response(response, parsed)
|
|
132
|
+
is_error = response.is_error or (self._raise_on_failure and not parsed.success)
|
|
133
|
+
if not is_error:
|
|
134
|
+
return parsed
|
|
135
|
+
|
|
136
|
+
api_error = ChatAdsAPIError(
|
|
137
|
+
status_code=response.status_code,
|
|
138
|
+
payload=parsed.raw,
|
|
139
|
+
response=parsed,
|
|
140
|
+
headers=dict(response.headers),
|
|
141
|
+
request_body=body,
|
|
142
|
+
url=url,
|
|
143
|
+
)
|
|
144
|
+
if attempt < self._max_retries and self._should_retry_status(response.status_code):
|
|
145
|
+
_sleep_sync(
|
|
146
|
+
_compute_retry_delay(attempt, self._retry_backoff_factor, api_error.retry_after)
|
|
147
|
+
)
|
|
148
|
+
attempt += 1
|
|
149
|
+
continue
|
|
150
|
+
raise api_error
|
|
151
|
+
|
|
152
|
+
def _should_retry_status(self, status_code: int) -> bool:
|
|
153
|
+
return status_code in self._retry_statuses
|
|
154
|
+
|
|
155
|
+
def _log_request(self, url: str, headers: Dict[str, str], body: Dict[str, Any]) -> None:
|
|
156
|
+
if not self._debug:
|
|
157
|
+
return
|
|
158
|
+
safe_headers = {k: v for k, v in headers.items() if k.lower() != "x-api-key"}
|
|
159
|
+
self._logger.info("ChatAds request -> %s", url)
|
|
160
|
+
self._logger.info("Headers: %s", safe_headers)
|
|
161
|
+
self._logger.info("Body: %s", json.dumps(body, indent=2))
|
|
162
|
+
|
|
163
|
+
def _log_response(self, response: httpx.Response, parsed: ChatAdsResponse) -> None:
|
|
164
|
+
if not self._debug:
|
|
165
|
+
return
|
|
166
|
+
self._logger.info(
|
|
167
|
+
"ChatAds response <- %s %s (status=%s)",
|
|
168
|
+
response.request.method if response.request else "POST",
|
|
169
|
+
response.request.url if response.request else "<unknown>",
|
|
170
|
+
response.status_code,
|
|
171
|
+
)
|
|
172
|
+
self._logger.info("Payload: %s", json.dumps(parsed.raw, indent=2))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AsyncChatAdsClient:
|
|
176
|
+
"""Asynchronous version backed by httpx.AsyncClient."""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
api_key: str,
|
|
181
|
+
base_url: str,
|
|
182
|
+
*,
|
|
183
|
+
endpoint: str = _DEFAULT_ENDPOINT,
|
|
184
|
+
timeout: float = 10.0,
|
|
185
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
186
|
+
raise_on_failure: bool = False,
|
|
187
|
+
max_retries: int = 0,
|
|
188
|
+
retry_backoff_factor: float = 0.5,
|
|
189
|
+
retry_statuses: Optional[Iterable[int]] = None,
|
|
190
|
+
logger: Optional[logging.Logger] = None,
|
|
191
|
+
debug: bool = False,
|
|
192
|
+
) -> None:
|
|
193
|
+
if not api_key:
|
|
194
|
+
raise ValueError("api_key is required")
|
|
195
|
+
if not base_url:
|
|
196
|
+
raise ValueError("base_url is required")
|
|
197
|
+
|
|
198
|
+
self._api_key = api_key
|
|
199
|
+
self._base_url = base_url.rstrip("/")
|
|
200
|
+
self._endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
201
|
+
self._timeout = timeout
|
|
202
|
+
self._client = http_client or httpx.AsyncClient(timeout=timeout)
|
|
203
|
+
self._owns_client = http_client is None
|
|
204
|
+
self._raise_on_failure = raise_on_failure
|
|
205
|
+
self._max_retries = max(0, int(max_retries))
|
|
206
|
+
self._retry_backoff_factor = max(0.0, float(retry_backoff_factor))
|
|
207
|
+
self._retry_statuses: Set[int] = (
|
|
208
|
+
set(retry_statuses) if retry_statuses is not None else set(_DEFAULT_RETRY_STATUSES)
|
|
209
|
+
)
|
|
210
|
+
self._logger = logger or logging.getLogger("chatads_sdk")
|
|
211
|
+
self._debug = debug
|
|
212
|
+
|
|
213
|
+
async def aclose(self) -> None:
|
|
214
|
+
if self._owns_client:
|
|
215
|
+
await self._client.aclose()
|
|
216
|
+
|
|
217
|
+
async def __aenter__(self) -> "AsyncChatAdsClient":
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # type: ignore[override]
|
|
221
|
+
await self.aclose()
|
|
222
|
+
|
|
223
|
+
async def analyze(
|
|
224
|
+
self,
|
|
225
|
+
payload: FunctionItemPayload,
|
|
226
|
+
*,
|
|
227
|
+
timeout: Optional[float] = None,
|
|
228
|
+
headers: Optional[Dict[str, str]] = None,
|
|
229
|
+
) -> ChatAdsResponse:
|
|
230
|
+
body = payload.to_payload()
|
|
231
|
+
return await self._post(body, timeout=timeout, headers=headers)
|
|
232
|
+
|
|
233
|
+
async def analyze_message(
|
|
234
|
+
self,
|
|
235
|
+
message: str,
|
|
236
|
+
*,
|
|
237
|
+
timeout: Optional[float] = None,
|
|
238
|
+
headers: Optional[Dict[str, str]] = None,
|
|
239
|
+
**extra_fields: Any,
|
|
240
|
+
) -> ChatAdsResponse:
|
|
241
|
+
payload = _build_payload_from_kwargs(message, extra_fields)
|
|
242
|
+
return await self.analyze(payload, timeout=timeout, headers=headers)
|
|
243
|
+
|
|
244
|
+
async def _post(
|
|
245
|
+
self,
|
|
246
|
+
body: Dict[str, Any],
|
|
247
|
+
*,
|
|
248
|
+
timeout: Optional[float],
|
|
249
|
+
headers: Optional[Dict[str, str]],
|
|
250
|
+
) -> ChatAdsResponse:
|
|
251
|
+
request_headers = {"x-api-key": self._api_key, **(headers or {})}
|
|
252
|
+
url = f"{self._base_url}{self._endpoint}"
|
|
253
|
+
attempt = 0
|
|
254
|
+
while True:
|
|
255
|
+
try:
|
|
256
|
+
self._log_request(url, request_headers, body)
|
|
257
|
+
response = await self._client.post(
|
|
258
|
+
url,
|
|
259
|
+
json=body,
|
|
260
|
+
headers=request_headers,
|
|
261
|
+
timeout=timeout or self._timeout,
|
|
262
|
+
)
|
|
263
|
+
except httpx.RequestError as exc:
|
|
264
|
+
if attempt >= self._max_retries:
|
|
265
|
+
raise ChatAdsSDKError(f"Transport error while calling ChatAds: {exc}") from exc
|
|
266
|
+
await _sleep_async(
|
|
267
|
+
_compute_retry_delay(attempt, self._retry_backoff_factor, None)
|
|
268
|
+
)
|
|
269
|
+
attempt += 1
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
parsed = _parse_response(response)
|
|
273
|
+
self._log_response(response, parsed)
|
|
274
|
+
is_error = response.is_error or (self._raise_on_failure and not parsed.success)
|
|
275
|
+
if not is_error:
|
|
276
|
+
return parsed
|
|
277
|
+
|
|
278
|
+
api_error = ChatAdsAPIError(
|
|
279
|
+
status_code=response.status_code,
|
|
280
|
+
payload=parsed.raw,
|
|
281
|
+
response=parsed,
|
|
282
|
+
headers=dict(response.headers),
|
|
283
|
+
request_body=body,
|
|
284
|
+
url=url,
|
|
285
|
+
)
|
|
286
|
+
if attempt < self._max_retries and self._should_retry_status(response.status_code):
|
|
287
|
+
await _sleep_async(
|
|
288
|
+
_compute_retry_delay(attempt, self._retry_backoff_factor, api_error.retry_after)
|
|
289
|
+
)
|
|
290
|
+
attempt += 1
|
|
291
|
+
continue
|
|
292
|
+
raise api_error
|
|
293
|
+
|
|
294
|
+
def _should_retry_status(self, status_code: int) -> bool:
|
|
295
|
+
return status_code in self._retry_statuses
|
|
296
|
+
|
|
297
|
+
def _log_request(self, url: str, headers: Dict[str, str], body: Dict[str, Any]) -> None:
|
|
298
|
+
if not self._debug:
|
|
299
|
+
return
|
|
300
|
+
safe_headers = {k: v for k, v in headers.items() if k.lower() != "x-api-key"}
|
|
301
|
+
self._logger.info("ChatAds request -> %s", url)
|
|
302
|
+
self._logger.info("Headers: %s", safe_headers)
|
|
303
|
+
self._logger.info("Body: %s", json.dumps(body, indent=2))
|
|
304
|
+
|
|
305
|
+
def _log_response(self, response: httpx.Response, parsed: ChatAdsResponse) -> None:
|
|
306
|
+
if not self._debug:
|
|
307
|
+
return
|
|
308
|
+
self._logger.info(
|
|
309
|
+
"ChatAds response <- %s %s (status=%s)",
|
|
310
|
+
response.request.method if response.request else "POST",
|
|
311
|
+
response.request.url if response.request else "<unknown>",
|
|
312
|
+
response.status_code,
|
|
313
|
+
)
|
|
314
|
+
self._logger.info("Payload: %s", json.dumps(parsed.raw, indent=2))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _parse_response(response: httpx.Response) -> ChatAdsResponse:
|
|
318
|
+
try:
|
|
319
|
+
payload = response.json()
|
|
320
|
+
except ValueError as exc:
|
|
321
|
+
raise ChatAdsSDKError("ChatAds returned a non-JSON response") from exc
|
|
322
|
+
return ChatAdsResponse.from_dict(payload)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _build_payload_from_kwargs(message: str, kwargs: Dict[str, Any]) -> FunctionItemPayload:
|
|
326
|
+
known: Dict[str, Any] = {}
|
|
327
|
+
extra: Dict[str, Any] = {}
|
|
328
|
+
for key, value in kwargs.items():
|
|
329
|
+
normalized = _normalize_field_name(key)
|
|
330
|
+
if normalized:
|
|
331
|
+
known[normalized] = value
|
|
332
|
+
else:
|
|
333
|
+
extra[key] = value
|
|
334
|
+
return FunctionItemPayload(message=message, extra_fields=extra, **known)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _normalize_field_name(field: str) -> Optional[str]:
|
|
338
|
+
if field in _FUNCTION_ITEM_OPTIONAL_FIELDS:
|
|
339
|
+
return field
|
|
340
|
+
return _FIELD_ALIAS_LOOKUP.get(field.lower())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _compute_retry_delay(
|
|
344
|
+
attempt: int,
|
|
345
|
+
backoff_factor: float,
|
|
346
|
+
retry_after_header: Optional[str],
|
|
347
|
+
) -> float:
|
|
348
|
+
header_delay = _parse_retry_after(retry_after_header)
|
|
349
|
+
if header_delay is not None:
|
|
350
|
+
return header_delay
|
|
351
|
+
if backoff_factor <= 0:
|
|
352
|
+
return 0.0
|
|
353
|
+
return backoff_factor * (2 ** attempt)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _parse_retry_after(header_value: Optional[str]) -> Optional[float]:
|
|
357
|
+
if header_value is None:
|
|
358
|
+
return None
|
|
359
|
+
header_value = header_value.strip()
|
|
360
|
+
if not header_value:
|
|
361
|
+
return None
|
|
362
|
+
try:
|
|
363
|
+
return max(0.0, float(header_value))
|
|
364
|
+
except ValueError:
|
|
365
|
+
pass
|
|
366
|
+
try:
|
|
367
|
+
dt = parsedate_to_datetime(header_value)
|
|
368
|
+
if dt is None:
|
|
369
|
+
return None
|
|
370
|
+
if dt.tzinfo is None:
|
|
371
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
372
|
+
seconds = (dt - datetime.now(timezone.utc)).total_seconds()
|
|
373
|
+
return max(0.0, seconds)
|
|
374
|
+
except (TypeError, ValueError, OverflowError):
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _sleep_sync(delay: float) -> None:
|
|
379
|
+
if delay > 0:
|
|
380
|
+
time.sleep(delay)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
async def _sleep_async(delay: float) -> None:
|
|
384
|
+
if delay > 0:
|
|
385
|
+
await asyncio.sleep(delay)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""SDK-specific exception hierarchy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from .models import ChatAdsResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChatAdsSDKError(Exception):
|
|
11
|
+
"""Base class for local SDK issues (serialization, transport, etc.)."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChatAdsAPIError(ChatAdsSDKError):
|
|
15
|
+
"""Raised for non-2xx responses returned by the ChatAds API."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
status_code: int,
|
|
20
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
21
|
+
response: Optional[ChatAdsResponse] = None,
|
|
22
|
+
headers: Optional[Dict[str, str]] = None,
|
|
23
|
+
request_body: Optional[Dict[str, Any]] = None,
|
|
24
|
+
url: Optional[str] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.payload = payload or {}
|
|
28
|
+
self.response = response
|
|
29
|
+
self.headers = headers or {}
|
|
30
|
+
self.request_body = request_body or {}
|
|
31
|
+
self.url = url
|
|
32
|
+
message = self._build_message()
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
|
|
35
|
+
def _build_message(self) -> str:
|
|
36
|
+
if self.response and self.response.error:
|
|
37
|
+
return (
|
|
38
|
+
f"ChatAds API error {self.status_code}: "
|
|
39
|
+
f"{self.response.error.code} - {self.response.error.message}"
|
|
40
|
+
)
|
|
41
|
+
return f"ChatAds API error {self.status_code}"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def retry_after(self) -> Optional[str]:
|
|
45
|
+
"""Expose Retry-After header when rate limits are hit."""
|
|
46
|
+
for key, value in self.headers.items():
|
|
47
|
+
if key.lower() == "retry-after":
|
|
48
|
+
return value
|
|
49
|
+
return None
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Dataclasses that mirror the ChatAds FastAPI request/response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
FUNCTION_ITEM_OPTIONAL_FIELDS = (
|
|
9
|
+
"page_url",
|
|
10
|
+
"page_title",
|
|
11
|
+
"referrer",
|
|
12
|
+
"address",
|
|
13
|
+
"email",
|
|
14
|
+
"type",
|
|
15
|
+
"domain",
|
|
16
|
+
"user_agent",
|
|
17
|
+
"ip",
|
|
18
|
+
"reason",
|
|
19
|
+
"company",
|
|
20
|
+
"name",
|
|
21
|
+
"country",
|
|
22
|
+
"language",
|
|
23
|
+
"website",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_CAMELCASE_ALIASES = {
|
|
27
|
+
"pageurl": "page_url",
|
|
28
|
+
"pagetitle": "page_title",
|
|
29
|
+
"useragent": "user_agent",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
FUNCTION_ITEM_FIELD_ALIASES = {
|
|
33
|
+
**{field: field for field in FUNCTION_ITEM_OPTIONAL_FIELDS},
|
|
34
|
+
**_CAMELCASE_ALIASES,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_FIELD_TO_PAYLOAD_KEY = {
|
|
38
|
+
"page_url": "pageUrl",
|
|
39
|
+
"page_title": "pageTitle",
|
|
40
|
+
"referrer": "referrer",
|
|
41
|
+
"address": "address",
|
|
42
|
+
"email": "email",
|
|
43
|
+
"type": "type",
|
|
44
|
+
"domain": "domain",
|
|
45
|
+
"user_agent": "userAgent",
|
|
46
|
+
"ip": "ip",
|
|
47
|
+
"reason": "reason",
|
|
48
|
+
"company": "company",
|
|
49
|
+
"name": "name",
|
|
50
|
+
"country": "country",
|
|
51
|
+
"language": "language",
|
|
52
|
+
"website": "website",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
RESERVED_PAYLOAD_KEYS = frozenset({"message", *(_FIELD_TO_PAYLOAD_KEY.values())})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ChatAdsAd:
|
|
60
|
+
product: str
|
|
61
|
+
link: str
|
|
62
|
+
message: str
|
|
63
|
+
category: str
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> Optional["ChatAdsAd"]:
|
|
67
|
+
if not data:
|
|
68
|
+
return None
|
|
69
|
+
return cls(
|
|
70
|
+
product=data.get("product", ""),
|
|
71
|
+
link=data.get("link", ""),
|
|
72
|
+
message=data.get("message", ""),
|
|
73
|
+
category=data.get("category", ""),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ChatAdsData:
|
|
79
|
+
matched: bool
|
|
80
|
+
ad: Optional[ChatAdsAd] = None
|
|
81
|
+
reason: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> Optional["ChatAdsData"]:
|
|
85
|
+
if not data:
|
|
86
|
+
return None
|
|
87
|
+
return cls(
|
|
88
|
+
matched=bool(data.get("matched", False)),
|
|
89
|
+
ad=ChatAdsAd.from_dict(data.get("ad")),
|
|
90
|
+
reason=data.get("reason"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ChatAdsError:
|
|
96
|
+
code: str
|
|
97
|
+
message: str
|
|
98
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> Optional["ChatAdsError"]:
|
|
102
|
+
if not data:
|
|
103
|
+
return None
|
|
104
|
+
return cls(
|
|
105
|
+
code=data.get("code", "UNKNOWN"),
|
|
106
|
+
message=data.get("message", ""),
|
|
107
|
+
details=data.get("details") or {},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class UsageInfo:
|
|
113
|
+
monthly_requests: int
|
|
114
|
+
free_tier_limit: int
|
|
115
|
+
free_tier_remaining: int
|
|
116
|
+
is_free_tier: bool
|
|
117
|
+
has_credit_card: bool
|
|
118
|
+
daily_requests: Optional[int] = None
|
|
119
|
+
daily_limit: Optional[int] = None
|
|
120
|
+
minute_requests: Optional[int] = None
|
|
121
|
+
minute_limit: Optional[int] = None
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> Optional["UsageInfo"]:
|
|
125
|
+
if not data:
|
|
126
|
+
return None
|
|
127
|
+
return cls(
|
|
128
|
+
monthly_requests=int(data.get("monthly_requests") or 0),
|
|
129
|
+
free_tier_limit=int(data.get("free_tier_limit") or 0),
|
|
130
|
+
free_tier_remaining=int(data.get("free_tier_remaining") or 0),
|
|
131
|
+
is_free_tier=bool(data.get("is_free_tier", False)),
|
|
132
|
+
has_credit_card=bool(data.get("has_credit_card", False)),
|
|
133
|
+
daily_requests=_maybe_int(data.get("daily_requests")),
|
|
134
|
+
daily_limit=_maybe_int(data.get("daily_limit")),
|
|
135
|
+
minute_requests=_maybe_int(data.get("minute_requests")),
|
|
136
|
+
minute_limit=_maybe_int(data.get("minute_limit")),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ChatAdsMeta:
|
|
142
|
+
request_id: str
|
|
143
|
+
user_id: Optional[str] = None
|
|
144
|
+
country: Optional[str] = None
|
|
145
|
+
language: Optional[str] = None
|
|
146
|
+
processing_time_ms: Optional[float] = None
|
|
147
|
+
usage: Optional[UsageInfo] = None
|
|
148
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dict(cls, data: Optional[Dict[str, Any]]) -> "ChatAdsMeta":
|
|
152
|
+
data = data or {}
|
|
153
|
+
return cls(
|
|
154
|
+
request_id=data.get("request_id", ""),
|
|
155
|
+
user_id=data.get("user_id"),
|
|
156
|
+
country=data.get("country"),
|
|
157
|
+
language=data.get("language"),
|
|
158
|
+
processing_time_ms=data.get("processing_time_ms"),
|
|
159
|
+
usage=UsageInfo.from_dict(data.get("usage")),
|
|
160
|
+
raw=data,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class ChatAdsResponse:
|
|
166
|
+
success: bool
|
|
167
|
+
meta: ChatAdsMeta
|
|
168
|
+
data: Optional[ChatAdsData] = None
|
|
169
|
+
error: Optional[ChatAdsError] = None
|
|
170
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ChatAdsResponse":
|
|
174
|
+
data = data or {}
|
|
175
|
+
return cls(
|
|
176
|
+
success=bool(data.get("success", False)),
|
|
177
|
+
data=ChatAdsData.from_dict(data.get("data")),
|
|
178
|
+
error=ChatAdsError.from_dict(data.get("error")),
|
|
179
|
+
meta=ChatAdsMeta.from_dict(data.get("meta")),
|
|
180
|
+
raw=data,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class FunctionItemPayload:
|
|
186
|
+
"""Subset of the server's FunctionItem pydantic model."""
|
|
187
|
+
|
|
188
|
+
message: str
|
|
189
|
+
page_url: Optional[str] = None
|
|
190
|
+
page_title: Optional[str] = None
|
|
191
|
+
referrer: Optional[str] = None
|
|
192
|
+
address: Optional[str] = None
|
|
193
|
+
email: Optional[str] = None
|
|
194
|
+
type: Optional[str] = None
|
|
195
|
+
domain: Optional[str] = None
|
|
196
|
+
user_agent: Optional[str] = None
|
|
197
|
+
ip: Optional[str] = None
|
|
198
|
+
reason: Optional[str] = None
|
|
199
|
+
company: Optional[str] = None
|
|
200
|
+
name: Optional[str] = None
|
|
201
|
+
country: Optional[str] = None
|
|
202
|
+
language: Optional[str] = None
|
|
203
|
+
website: Optional[str] = None
|
|
204
|
+
extra_fields: Dict[str, Any] = field(default_factory=dict)
|
|
205
|
+
|
|
206
|
+
def to_payload(self) -> Dict[str, Any]:
|
|
207
|
+
payload = {"message": self.message}
|
|
208
|
+
for field_name, payload_key in _FIELD_TO_PAYLOAD_KEY.items():
|
|
209
|
+
value = getattr(self, field_name)
|
|
210
|
+
if value is not None:
|
|
211
|
+
payload[payload_key] = value
|
|
212
|
+
|
|
213
|
+
conflicts = RESERVED_PAYLOAD_KEYS.intersection(self.extra_fields.keys())
|
|
214
|
+
if conflicts:
|
|
215
|
+
conflict_list = ", ".join(sorted(conflicts))
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"extra_fields contains reserved keys that would override core payload data: {conflict_list}"
|
|
218
|
+
)
|
|
219
|
+
payload.update(self.extra_fields)
|
|
220
|
+
return payload
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _maybe_int(value: Any) -> Optional[int]:
|
|
224
|
+
try:
|
|
225
|
+
return int(value)
|
|
226
|
+
except (TypeError, ValueError):
|
|
227
|
+
return None
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatads-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Python client for the ChatAds affiliate scoring API
|
|
5
|
+
Author-email: ChatAds <support@getchatads.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
9
|
+
Provides-Extra: async
|
|
10
|
+
Requires-Dist: httpx[http2]<1.0,>=0.27; extra == "async"
|
|
11
|
+
|
|
12
|
+
# ChatAds Python SDK
|
|
13
|
+
|
|
14
|
+
A tiny, dependency-light wrapper around the ChatAds `/v1/chatads-script` endpoint. It mirrors the response payloads returned by the FastAPI service so you can drop it into CLIs, serverless functions, or orchestration tools.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
(Or build/upload to your internal index as needed.)
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from chatads_sdk import ChatAdsClient, FunctionItemPayload
|
|
28
|
+
|
|
29
|
+
client = ChatAdsClient(
|
|
30
|
+
api_key="YOUR_X_API_KEY",
|
|
31
|
+
base_url="https://<your-chatads-domain>",
|
|
32
|
+
raise_on_failure=True, # Treat success=False payloads as exceptions
|
|
33
|
+
max_retries=2, # Optional automatic retries for 429/5xx responses
|
|
34
|
+
retry_backoff_factor=0.75, # Exponential backoff multiplier
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
payload = FunctionItemPayload(
|
|
38
|
+
message="Looking for a CRM to close more deals",
|
|
39
|
+
ip="1.2.3.4",
|
|
40
|
+
user_agent="Mozilla/5.0",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = client.analyze(payload)
|
|
44
|
+
|
|
45
|
+
if result.success:
|
|
46
|
+
print(result.data.ad)
|
|
47
|
+
else:
|
|
48
|
+
print(result.error.code, result.error.message)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Error Handling
|
|
52
|
+
|
|
53
|
+
Non-2xx responses raise `ChatAdsAPIError` and include the parsed error payload plus the original HTTP status code so you can branch on quota/validation failures. Set `raise_on_failure=True` if you want 200 responses with `success=false` to raise the same exception class.
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- Retries are opt-in. Provide `max_retries>0` to automatically retry transport errors and retryable status codes. The client honors `Retry-After` headers and falls back to exponential backoff.
|
|
58
|
+
- `FunctionItemPayload` matches the server-side `FunctionItem` pydantic model. Keyword arguments passed to `ChatAdsClient.analyze_message()` accept either snake_case (`user_agent`) or camelCase (`userAgent`) keys.
|
|
59
|
+
- Reserved payload keys (e.g., `message`, `pageUrl`, `userAgent`) cannot be overridden through `extra_fields`; doing so raises `ValueError` to prevent silent mutations.
|
|
60
|
+
|
|
61
|
+
## CLI Smoke Test
|
|
62
|
+
|
|
63
|
+
For a super-quick check, edit the config block at the top of `python_sdk/run_sdk_smoke.py` (or set the
|
|
64
|
+
`CHATADS_*` env vars) and run:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
PYTHONPATH=python_sdk python python_sdk/run_sdk_smoke.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It prints the raw JSON response or surfaces a `ChatAdsAPIError` with status/error fields so you can see
|
|
71
|
+
exactly what the API returned.
|
|
72
|
+
|
|
73
|
+
- `API_KEY` and `MESSAGE` are the only required values. Leave `CALLER_IP`, `USER_AGENT`, or `CHATADS_EXTRA_FIELDS`
|
|
74
|
+
blank to omit them from the request.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
chatads_sdk/__init__.py
|
|
4
|
+
chatads_sdk/client.py
|
|
5
|
+
chatads_sdk/exceptions.py
|
|
6
|
+
chatads_sdk/models.py
|
|
7
|
+
chatads_sdk.egg-info/PKG-INFO
|
|
8
|
+
chatads_sdk.egg-info/SOURCES.txt
|
|
9
|
+
chatads_sdk.egg-info/dependency_links.txt
|
|
10
|
+
chatads_sdk.egg-info/requires.txt
|
|
11
|
+
chatads_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chatads_sdk
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "chatads-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight Python client for the ChatAds affiliate scoring API"
|
|
5
|
+
authors = [{name = "ChatAds", email = "support@getchatads.com"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.27,<1.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
async = ["httpx[http2]>=0.27,<1.0"]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["setuptools>=68"]
|
|
17
|
+
build-backend = "setuptools.build_meta"
|