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 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"