smartlyq 0.1.2__py3-none-any.whl
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.
- smartlyq/__init__.py +60 -0
- smartlyq/_core.py +147 -0
- smartlyq/_version.py +1 -0
- smartlyq/resources.py +908 -0
- smartlyq-0.1.2.dist-info/METADATA +418 -0
- smartlyq-0.1.2.dist-info/RECORD +8 -0
- smartlyq-0.1.2.dist-info/WHEEL +4 -0
- smartlyq-0.1.2.dist-info/licenses/LICENSE +21 -0
smartlyq/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""SmartlyQ Python SDK.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
from smartlyq import SmartlyQ
|
|
5
|
+
|
|
6
|
+
sq = SmartlyQ(api_key="sqk_live_...")
|
|
7
|
+
me = sq.account.get_me()
|
|
8
|
+
post = sq.social.create_post({"text": "Hello!", "account_ids": ["acc_123"]})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from ._core import CoreClient, SmartlyQConnectionError, SmartlyQError
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
from .resources import create_resources
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SmartlyQ:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: Optional[str] = None,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str = "https://api.smartlyq.com/v1",
|
|
28
|
+
timeout: float = 60.0,
|
|
29
|
+
max_retries: int = 2,
|
|
30
|
+
default_headers: Optional[dict[str, str]] = None,
|
|
31
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
32
|
+
):
|
|
33
|
+
self.core = CoreClient(
|
|
34
|
+
api_key,
|
|
35
|
+
base_url=base_url,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
max_retries=max_retries,
|
|
38
|
+
default_headers=default_headers,
|
|
39
|
+
transport=transport,
|
|
40
|
+
)
|
|
41
|
+
for key, resource in create_resources(self.core).items():
|
|
42
|
+
setattr(self, key, resource)
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
self.core.close()
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> "SmartlyQ":
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, *exc: object) -> None:
|
|
51
|
+
self.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"SmartlyQ",
|
|
56
|
+
"SmartlyQError",
|
|
57
|
+
"SmartlyQConnectionError",
|
|
58
|
+
"CoreClient",
|
|
59
|
+
"__version__",
|
|
60
|
+
]
|
smartlyq/_core.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""SmartlyQ SDK core HTTP client.
|
|
2
|
+
|
|
3
|
+
Hand-written; the resource surface in ``resources.py`` is generated on top of it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
|
|
17
|
+
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SmartlyQError(Exception):
|
|
21
|
+
"""Raised for any non-2xx API response."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, status_code: int, envelope: Optional[dict], fallback: str):
|
|
24
|
+
error = (envelope or {}).get("error") or {}
|
|
25
|
+
meta = (envelope or {}).get("meta") or {}
|
|
26
|
+
super().__init__(error.get("message") or fallback)
|
|
27
|
+
self.status_code: int = status_code
|
|
28
|
+
self.code: Optional[str] = error.get("code")
|
|
29
|
+
self.details: Optional[dict] = error.get("details")
|
|
30
|
+
self.request_id: Optional[str] = meta.get("request_id")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SmartlyQConnectionError(Exception):
|
|
34
|
+
"""Raised when a request cannot reach the API (network failure, timeout)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CoreClient:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: Optional[str] = None,
|
|
41
|
+
*,
|
|
42
|
+
base_url: str = "https://api.smartlyq.com/v1",
|
|
43
|
+
timeout: float = 60.0,
|
|
44
|
+
max_retries: int = 2,
|
|
45
|
+
default_headers: Optional[dict[str, str]] = None,
|
|
46
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
47
|
+
):
|
|
48
|
+
api_key = api_key or os.environ.get("SMARTLYQ_API_KEY")
|
|
49
|
+
if not api_key:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Missing API key. Pass api_key to the client or set the "
|
|
52
|
+
"SMARTLYQ_API_KEY environment variable."
|
|
53
|
+
)
|
|
54
|
+
self._api_key = api_key
|
|
55
|
+
self._base_url = base_url.rstrip("/")
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._max_retries = max_retries
|
|
58
|
+
self._default_headers = default_headers or {}
|
|
59
|
+
self._client = httpx.Client(transport=transport, timeout=timeout)
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
self._client.close()
|
|
63
|
+
|
|
64
|
+
def __enter__(self) -> "CoreClient":
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, *exc: object) -> None:
|
|
68
|
+
self.close()
|
|
69
|
+
|
|
70
|
+
def request(
|
|
71
|
+
self,
|
|
72
|
+
method: str,
|
|
73
|
+
path: str,
|
|
74
|
+
*,
|
|
75
|
+
body: Optional[dict] = None,
|
|
76
|
+
query: Optional[dict] = None,
|
|
77
|
+
profile_id: Optional[str] = None,
|
|
78
|
+
idempotency_key: Optional[str] = None,
|
|
79
|
+
timeout: Optional[float] = None,
|
|
80
|
+
headers: Optional[dict[str, str]] = None,
|
|
81
|
+
) -> Any:
|
|
82
|
+
url = self._base_url + path
|
|
83
|
+
request_headers: dict[str, str] = {
|
|
84
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
85
|
+
"Accept": "application/json",
|
|
86
|
+
"User-Agent": f"smartlyq-python/{__version__}",
|
|
87
|
+
**self._default_headers,
|
|
88
|
+
**(headers or {}),
|
|
89
|
+
}
|
|
90
|
+
if profile_id:
|
|
91
|
+
request_headers["X-Profile-Id"] = profile_id
|
|
92
|
+
if idempotency_key:
|
|
93
|
+
request_headers["Idempotency-Key"] = idempotency_key
|
|
94
|
+
|
|
95
|
+
params = {k: v for k, v in (query or {}).items() if v is not None}
|
|
96
|
+
last_exc: Optional[Exception] = None
|
|
97
|
+
|
|
98
|
+
for attempt in range(self._max_retries + 1):
|
|
99
|
+
try:
|
|
100
|
+
response = self._client.request(
|
|
101
|
+
method,
|
|
102
|
+
url,
|
|
103
|
+
json=body,
|
|
104
|
+
params=params or None,
|
|
105
|
+
headers=request_headers,
|
|
106
|
+
timeout=timeout if timeout is not None else self._timeout,
|
|
107
|
+
)
|
|
108
|
+
except httpx.HTTPError as exc:
|
|
109
|
+
last_exc = exc
|
|
110
|
+
if attempt < self._max_retries:
|
|
111
|
+
time.sleep(_backoff(attempt))
|
|
112
|
+
continue
|
|
113
|
+
raise SmartlyQConnectionError(f"Request failed: {exc}") from exc
|
|
114
|
+
|
|
115
|
+
if response.is_success:
|
|
116
|
+
if response.status_code == 204 or not response.content:
|
|
117
|
+
return None
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
envelope = response.json()
|
|
122
|
+
except ValueError:
|
|
123
|
+
envelope = None
|
|
124
|
+
|
|
125
|
+
if response.status_code in RETRYABLE_STATUSES and attempt < self._max_retries:
|
|
126
|
+
retry_after = _parse_retry_after(response.headers.get("Retry-After"))
|
|
127
|
+
time.sleep(retry_after if retry_after is not None else _backoff(attempt))
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
raise SmartlyQError(response.status_code, envelope, f"HTTP {response.status_code}")
|
|
131
|
+
|
|
132
|
+
raise SmartlyQConnectionError(f"Request failed: {last_exc}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _backoff(attempt: int) -> float:
|
|
136
|
+
base = 0.5 * (2**attempt)
|
|
137
|
+
return base + random.random() * base * 0.25
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[float]:
|
|
141
|
+
if not value:
|
|
142
|
+
return None
|
|
143
|
+
try:
|
|
144
|
+
seconds = float(value)
|
|
145
|
+
except ValueError:
|
|
146
|
+
return None
|
|
147
|
+
return seconds if seconds > 0 else None
|
smartlyq/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|