softsolz 0.1.0__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.
- softsolz/__init__.py +35 -0
- softsolz/_client.py +60 -0
- softsolz/_http.py +153 -0
- softsolz/_version.py +1 -0
- softsolz/errors.py +91 -0
- softsolz/pagination.py +39 -0
- softsolz/py.typed +0 -0
- softsolz/resources/__init__.py +37 -0
- softsolz/resources/blogs.py +50 -0
- softsolz/resources/customer_auth.py +69 -0
- softsolz/resources/forms.py +45 -0
- softsolz/resources/invoicing.py +75 -0
- softsolz/resources/payments.py +27 -0
- softsolz/resources/smart_chat.py +87 -0
- softsolz/resources/social.py +32 -0
- softsolz/resources/workflows.py +45 -0
- softsolz/webhooks.py +75 -0
- softsolz-0.1.0.dist-info/METADATA +130 -0
- softsolz-0.1.0.dist-info/RECORD +20 -0
- softsolz-0.1.0.dist-info/WHEEL +4 -0
softsolz/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from . import webhooks
|
|
2
|
+
from ._client import SoftSolz
|
|
3
|
+
from ._http import DEFAULT_BASE_URL
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .errors import (
|
|
6
|
+
APIConnectionError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
ConflictError,
|
|
9
|
+
InvalidRequestError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
PaymentRequiredError,
|
|
12
|
+
PermissionDeniedError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
SoftSolzError,
|
|
15
|
+
WebhookSignatureError,
|
|
16
|
+
)
|
|
17
|
+
from .pagination import Page
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SoftSolz",
|
|
21
|
+
"Page",
|
|
22
|
+
"webhooks",
|
|
23
|
+
"DEFAULT_BASE_URL",
|
|
24
|
+
"SoftSolzError",
|
|
25
|
+
"InvalidRequestError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"PermissionDeniedError",
|
|
28
|
+
"PaymentRequiredError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"ConflictError",
|
|
31
|
+
"RateLimitError",
|
|
32
|
+
"APIConnectionError",
|
|
33
|
+
"WebhookSignatureError",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
softsolz/_client.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ._http import DEFAULT_BASE_URL, HttpClient
|
|
6
|
+
from .resources import build_resources
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SoftSolz:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
api_key: str,
|
|
13
|
+
*,
|
|
14
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
15
|
+
timeout: float = 30.0,
|
|
16
|
+
max_retries: int = 2,
|
|
17
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self._http = HttpClient(
|
|
20
|
+
api_key,
|
|
21
|
+
base_url=base_url,
|
|
22
|
+
timeout=timeout,
|
|
23
|
+
max_retries=max_retries,
|
|
24
|
+
transport=transport,
|
|
25
|
+
)
|
|
26
|
+
for name, resource in build_resources(self._http).items():
|
|
27
|
+
setattr(self, name, resource)
|
|
28
|
+
|
|
29
|
+
def whoami(self, **options: Any) -> Any:
|
|
30
|
+
return self._http.request("GET", "/api/v1/whoami", options=options or None)
|
|
31
|
+
|
|
32
|
+
def services(self, **options: Any) -> Any:
|
|
33
|
+
return self._http.request("GET", "/api/v1/services", options=options or None)
|
|
34
|
+
|
|
35
|
+
def service_health(self, service_id: str, **options: Any) -> Any:
|
|
36
|
+
from ._http import _q
|
|
37
|
+
|
|
38
|
+
return self._http.request(
|
|
39
|
+
"GET", f"/api/v1/services/{_q(service_id)}/health", options=options or None
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def request(
|
|
43
|
+
self,
|
|
44
|
+
method: str,
|
|
45
|
+
path: str,
|
|
46
|
+
*,
|
|
47
|
+
query: Optional[Dict[str, Any]] = None,
|
|
48
|
+
body: Optional[Dict[str, Any]] = None,
|
|
49
|
+
**options: Any,
|
|
50
|
+
) -> Any:
|
|
51
|
+
return self._http.request(method, path, query=query, body=body, options=options or None)
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
self._http.close()
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> "SoftSolz":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, *exc_info: Any) -> None:
|
|
60
|
+
self.close()
|
softsolz/_http.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .errors import APIConnectionError, error_from_response
|
|
11
|
+
from .pagination import Page
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://app.softsolz.uk"
|
|
14
|
+
|
|
15
|
+
_MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _q(value: Any) -> str:
|
|
19
|
+
return quote(str(value), safe="")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _backoff_seconds(attempt: int) -> float:
|
|
23
|
+
return min(8.0, 0.5 * (2**attempt)) + random.random() * 0.25
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HttpClient:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: str,
|
|
30
|
+
*,
|
|
31
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
32
|
+
timeout: float = 30.0,
|
|
33
|
+
max_retries: int = 2,
|
|
34
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
if not api_key or not isinstance(api_key, str):
|
|
37
|
+
raise ValueError('SoftSolz: api_key is required, e.g. SoftSolz(api_key="sk_test_...")')
|
|
38
|
+
self._max_retries = max_retries
|
|
39
|
+
self._client = httpx.Client(
|
|
40
|
+
base_url=base_url.rstrip("/"),
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
transport=transport,
|
|
43
|
+
headers={
|
|
44
|
+
"authorization": f"Bearer {api_key}",
|
|
45
|
+
"accept": "application/json",
|
|
46
|
+
"user-agent": f"softsolz-python/{__version__}",
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def envelope(
|
|
51
|
+
self,
|
|
52
|
+
method: str,
|
|
53
|
+
path: str,
|
|
54
|
+
*,
|
|
55
|
+
query: Optional[Dict[str, Any]] = None,
|
|
56
|
+
body: Optional[Dict[str, Any]] = None,
|
|
57
|
+
response_kind: str = "json",
|
|
58
|
+
options: Optional[Dict[str, Any]] = None,
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
options = options or {}
|
|
61
|
+
method = method.upper()
|
|
62
|
+
headers: Dict[str, str] = {}
|
|
63
|
+
if options.get("request_id"):
|
|
64
|
+
headers["x-request-id"] = str(options["request_id"])
|
|
65
|
+
if method in _MUTATING_METHODS:
|
|
66
|
+
headers["idempotency-key"] = str(options.get("idempotency_key") or uuid.uuid4())
|
|
67
|
+
params = {k: v for k, v in (query or {}).items() if v is not None}
|
|
68
|
+
max_retries = int(options.get("max_retries", self._max_retries))
|
|
69
|
+
timeout = options.get("timeout")
|
|
70
|
+
attempt = 0
|
|
71
|
+
while True:
|
|
72
|
+
try:
|
|
73
|
+
response = self._client.request(
|
|
74
|
+
method,
|
|
75
|
+
path,
|
|
76
|
+
params=params,
|
|
77
|
+
json=body,
|
|
78
|
+
headers=headers,
|
|
79
|
+
timeout=timeout if timeout is not None else httpx.USE_CLIENT_DEFAULT,
|
|
80
|
+
)
|
|
81
|
+
except httpx.HTTPError as exc:
|
|
82
|
+
if attempt < max_retries:
|
|
83
|
+
time.sleep(_backoff_seconds(attempt))
|
|
84
|
+
attempt += 1
|
|
85
|
+
continue
|
|
86
|
+
raise APIConnectionError(str(exc)) from exc
|
|
87
|
+
request_id = response.headers.get("softsolz-request-id")
|
|
88
|
+
if response.is_success:
|
|
89
|
+
if response_kind == "none" or response.status_code == 204:
|
|
90
|
+
return {"data": None, "meta": None, "request_id": request_id}
|
|
91
|
+
if response_kind == "text":
|
|
92
|
+
return {"data": response.text, "meta": None, "request_id": request_id}
|
|
93
|
+
if response_kind == "binary":
|
|
94
|
+
return {"data": response.content, "meta": None, "request_id": request_id}
|
|
95
|
+
payload = self._safe_json(response)
|
|
96
|
+
if isinstance(payload, dict) and "data" in payload:
|
|
97
|
+
data = payload.get("data")
|
|
98
|
+
meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else None
|
|
99
|
+
else:
|
|
100
|
+
data = payload
|
|
101
|
+
meta = None
|
|
102
|
+
return {"data": data, "meta": meta, "request_id": request_id}
|
|
103
|
+
retryable = response.status_code == 429 or response.status_code >= 500
|
|
104
|
+
if retryable and attempt < max_retries:
|
|
105
|
+
retry_after = response.headers.get("retry-after")
|
|
106
|
+
if retry_after and retry_after.isdigit() and int(retry_after) > 0:
|
|
107
|
+
delay = float(retry_after)
|
|
108
|
+
else:
|
|
109
|
+
delay = _backoff_seconds(attempt)
|
|
110
|
+
time.sleep(delay)
|
|
111
|
+
attempt += 1
|
|
112
|
+
continue
|
|
113
|
+
raise error_from_response(response.status_code, self._safe_json(response), request_id)
|
|
114
|
+
|
|
115
|
+
def request(
|
|
116
|
+
self,
|
|
117
|
+
method: str,
|
|
118
|
+
path: str,
|
|
119
|
+
*,
|
|
120
|
+
query: Optional[Dict[str, Any]] = None,
|
|
121
|
+
body: Optional[Dict[str, Any]] = None,
|
|
122
|
+
response_kind: str = "json",
|
|
123
|
+
options: Optional[Dict[str, Any]] = None,
|
|
124
|
+
) -> Any:
|
|
125
|
+
return self.envelope(
|
|
126
|
+
method, path, query=query, body=body, response_kind=response_kind, options=options
|
|
127
|
+
)["data"]
|
|
128
|
+
|
|
129
|
+
def page(
|
|
130
|
+
self,
|
|
131
|
+
method: str,
|
|
132
|
+
path: str,
|
|
133
|
+
*,
|
|
134
|
+
query: Optional[Dict[str, Any]] = None,
|
|
135
|
+
options: Optional[Dict[str, Any]] = None,
|
|
136
|
+
) -> Page:
|
|
137
|
+
def fetch_page(offset: Optional[int]) -> Dict[str, Any]:
|
|
138
|
+
merged = dict(query or {})
|
|
139
|
+
if offset is not None:
|
|
140
|
+
merged["offset"] = offset
|
|
141
|
+
return self.envelope(method, path, query=merged, options=options)
|
|
142
|
+
|
|
143
|
+
return Page.create(fetch_page)
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _safe_json(response: httpx.Response) -> Any:
|
|
147
|
+
try:
|
|
148
|
+
return response.json()
|
|
149
|
+
except ValueError:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
self._client.close()
|
softsolz/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
softsolz/errors.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SoftSolzError(Exception):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
message: str,
|
|
8
|
+
*,
|
|
9
|
+
status: int = 0,
|
|
10
|
+
code: str = "unknown_error",
|
|
11
|
+
details: Optional[Dict[str, Any]] = None,
|
|
12
|
+
request_id: Optional[str] = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
self.status = status
|
|
17
|
+
self.code = code
|
|
18
|
+
self.details = details
|
|
19
|
+
self.request_id = request_id
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidRequestError(SoftSolzError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuthenticationError(SoftSolzError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PermissionDeniedError(SoftSolzError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PaymentRequiredError(SoftSolzError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotFoundError(SoftSolzError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConflictError(SoftSolzError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RateLimitError(SoftSolzError):
|
|
47
|
+
def __init__(self, message: str, *, retry_after_seconds: Optional[int] = None, **kwargs: Any) -> None:
|
|
48
|
+
super().__init__(message, **kwargs)
|
|
49
|
+
self.retry_after_seconds = retry_after_seconds
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class APIConnectionError(SoftSolzError):
|
|
53
|
+
def __init__(self, message: str) -> None:
|
|
54
|
+
super().__init__(message, status=0, code="connection_error")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WebhookSignatureError(Exception):
|
|
58
|
+
def __init__(self, reason: str) -> None:
|
|
59
|
+
super().__init__(f"Webhook signature verification failed: {reason}")
|
|
60
|
+
self.reason = reason
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_STATUS_TO_ERROR = {
|
|
64
|
+
400: InvalidRequestError,
|
|
65
|
+
401: AuthenticationError,
|
|
66
|
+
402: PaymentRequiredError,
|
|
67
|
+
403: PermissionDeniedError,
|
|
68
|
+
404: NotFoundError,
|
|
69
|
+
409: ConflictError,
|
|
70
|
+
422: InvalidRequestError,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def error_from_response(status: int, body: Any, request_id: Optional[str] = None) -> SoftSolzError:
|
|
75
|
+
err = body.get("error") if isinstance(body, dict) else None
|
|
76
|
+
err = err if isinstance(err, dict) else {}
|
|
77
|
+
code = err.get("code") if isinstance(err.get("code"), str) else "unknown_error"
|
|
78
|
+
message = err.get("message") if isinstance(err.get("message"), str) else f"HTTP {status}"
|
|
79
|
+
details = err.get("details") if isinstance(err.get("details"), dict) else None
|
|
80
|
+
if status == 429:
|
|
81
|
+
retry_after = details.get("retry_after_seconds") if details else None
|
|
82
|
+
return RateLimitError(
|
|
83
|
+
message,
|
|
84
|
+
status=status,
|
|
85
|
+
code=code,
|
|
86
|
+
details=details,
|
|
87
|
+
request_id=request_id,
|
|
88
|
+
retry_after_seconds=retry_after if isinstance(retry_after, int) else None,
|
|
89
|
+
)
|
|
90
|
+
error_class = _STATUS_TO_ERROR.get(status, SoftSolzError)
|
|
91
|
+
return error_class(message, status=status, code=code, details=details, request_id=request_id)
|
softsolz/pagination.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Page:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
data: List[Any],
|
|
8
|
+
meta: Optional[Dict[str, Any]],
|
|
9
|
+
fetch_page: Callable[[Optional[int]], Dict[str, Any]],
|
|
10
|
+
) -> None:
|
|
11
|
+
self.data = data
|
|
12
|
+
self.meta = meta
|
|
13
|
+
self._fetch_page = fetch_page
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def create(cls, fetch_page: Callable[[Optional[int]], Dict[str, Any]]) -> "Page":
|
|
17
|
+
envelope = fetch_page(None)
|
|
18
|
+
return cls(envelope.get("data") or [], envelope.get("meta"), fetch_page)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def has_more(self) -> bool:
|
|
22
|
+
return bool(self.meta) and self.meta.get("has_more") is True
|
|
23
|
+
|
|
24
|
+
def next_page(self) -> Optional["Page"]:
|
|
25
|
+
if not self.has_more or not self.meta:
|
|
26
|
+
return None
|
|
27
|
+
offset = int(self.meta.get("offset", 0)) + int(self.meta.get("limit", len(self.data)))
|
|
28
|
+
envelope = self._fetch_page(offset)
|
|
29
|
+
return Page(envelope.get("data") or [], envelope.get("meta"), self._fetch_page)
|
|
30
|
+
|
|
31
|
+
def __iter__(self) -> Iterator[Any]:
|
|
32
|
+
page: Optional[Page] = self
|
|
33
|
+
while page is not None:
|
|
34
|
+
for item in page.data:
|
|
35
|
+
yield item
|
|
36
|
+
page = page.next_page()
|
|
37
|
+
|
|
38
|
+
def auto_paging_iter(self) -> Iterator[Any]:
|
|
39
|
+
return iter(self)
|
softsolz/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient
|
|
4
|
+
from .blogs import BlogsResource
|
|
5
|
+
from .customer_auth import CustomerAuthResource
|
|
6
|
+
from .forms import FormsResource
|
|
7
|
+
from .invoicing import InvoicingResource
|
|
8
|
+
from .payments import PaymentsResource
|
|
9
|
+
from .smart_chat import SmartChatResource
|
|
10
|
+
from .social import SocialResource
|
|
11
|
+
from .workflows import WorkflowsResource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_resources(http: HttpClient) -> Dict[str, Any]:
|
|
15
|
+
return {
|
|
16
|
+
"blogs": BlogsResource(http),
|
|
17
|
+
"customer_auth": CustomerAuthResource(http),
|
|
18
|
+
"forms": FormsResource(http),
|
|
19
|
+
"invoicing": InvoicingResource(http),
|
|
20
|
+
"payments": PaymentsResource(http),
|
|
21
|
+
"smart_chat": SmartChatResource(http),
|
|
22
|
+
"social": SocialResource(http),
|
|
23
|
+
"workflows": WorkflowsResource(http),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"build_resources",
|
|
29
|
+
"BlogsResource",
|
|
30
|
+
"CustomerAuthResource",
|
|
31
|
+
"FormsResource",
|
|
32
|
+
"InvoicingResource",
|
|
33
|
+
"PaymentsResource",
|
|
34
|
+
"SmartChatResource",
|
|
35
|
+
"SocialResource",
|
|
36
|
+
"WorkflowsResource",
|
|
37
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BlogsResource:
|
|
7
|
+
def __init__(self, http: HttpClient) -> None:
|
|
8
|
+
self._http = http
|
|
9
|
+
|
|
10
|
+
def list_published_posts(self, **options: Any) -> Any:
|
|
11
|
+
return self._http.request("GET", "/api/v1/services/blogs/posts/published", options=options or None)
|
|
12
|
+
|
|
13
|
+
def get_published_post_by_slug(self, slug: Any, **options: Any) -> Any:
|
|
14
|
+
return self._http.request("GET", f"/api/v1/services/blogs/posts/published/{_q(slug)}", options=options or None)
|
|
15
|
+
|
|
16
|
+
def get_posts(self, **options: Any) -> Any:
|
|
17
|
+
return self._http.request("GET", "/api/v1/services/blogs/posts", options=options or None)
|
|
18
|
+
|
|
19
|
+
def get_post(self, id: Any, **options: Any) -> Any:
|
|
20
|
+
return self._http.request("GET", f"/api/v1/services/blogs/posts/{_q(id)}", options=options or None)
|
|
21
|
+
|
|
22
|
+
def create_post(self, **options: Any) -> Any:
|
|
23
|
+
return self._http.request("POST", "/api/v1/services/blogs/posts", options=options or None)
|
|
24
|
+
|
|
25
|
+
def update_post(self, id: Any, **options: Any) -> Any:
|
|
26
|
+
return self._http.request("PATCH", f"/api/v1/services/blogs/posts/{_q(id)}", options=options or None)
|
|
27
|
+
|
|
28
|
+
def update_post_status(self, id: Any, **options: Any) -> Any:
|
|
29
|
+
return self._http.request("POST", f"/api/v1/services/blogs/posts/{_q(id)}/status", options=options or None)
|
|
30
|
+
|
|
31
|
+
def delete_post(self, id: Any, **options: Any) -> Any:
|
|
32
|
+
return self._http.request("DELETE", f"/api/v1/services/blogs/posts/{_q(id)}", options=options or None)
|
|
33
|
+
|
|
34
|
+
def get_categories(self, **options: Any) -> Any:
|
|
35
|
+
return self._http.request("GET", "/api/v1/services/blogs/categories", options=options or None)
|
|
36
|
+
|
|
37
|
+
def get_analytics(self, **options: Any) -> Any:
|
|
38
|
+
return self._http.request("GET", "/api/v1/services/blogs/analytics", options=options or None)
|
|
39
|
+
|
|
40
|
+
def get_subscribers(self, **options: Any) -> Any:
|
|
41
|
+
return self._http.request("GET", "/api/v1/services/blogs/subscribers", options=options or None)
|
|
42
|
+
|
|
43
|
+
def create_subscriber(self, **options: Any) -> Any:
|
|
44
|
+
return self._http.request("POST", "/api/v1/services/blogs/subscribers", options=options or None)
|
|
45
|
+
|
|
46
|
+
def update_subscriber(self, id: Any, **options: Any) -> Any:
|
|
47
|
+
return self._http.request("PATCH", f"/api/v1/services/blogs/subscribers/{_q(id)}", options=options or None)
|
|
48
|
+
|
|
49
|
+
def delete_subscriber(self, id: Any, **options: Any) -> Any:
|
|
50
|
+
return self._http.request("DELETE", f"/api/v1/services/blogs/subscribers/{_q(id)}", options=options or None)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CustomerAuthResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def list_users(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
12
|
+
return self._http.page("GET", "/api/v1/services/customer-auth/users", query=query, options=options or None)
|
|
13
|
+
|
|
14
|
+
def get_user(self, id: Any, **options: Any) -> Any:
|
|
15
|
+
return self._http.request("GET", f"/api/v1/services/customer-auth/users/{_q(id)}", options=options or None)
|
|
16
|
+
|
|
17
|
+
def list_user_sessions(self, id: Any, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
18
|
+
return self._http.page("GET", f"/api/v1/services/customer-auth/users/{_q(id)}/sessions", query=query, options=options or None)
|
|
19
|
+
|
|
20
|
+
def create_user(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/users", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def update_user(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
24
|
+
return self._http.request("PATCH", f"/api/v1/services/customer-auth/users/{_q(id)}", body=body, options=options or None)
|
|
25
|
+
|
|
26
|
+
def delete_user(self, id: Any, **options: Any) -> None:
|
|
27
|
+
self._http.request("DELETE", f"/api/v1/services/customer-auth/users/{_q(id)}", response_kind="none", options=options or None)
|
|
28
|
+
|
|
29
|
+
def logout_user(self, id: Any, **options: Any) -> Any:
|
|
30
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/logout", options=options or None)
|
|
31
|
+
|
|
32
|
+
def verify_session(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
33
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/sessions/verify", body=body, options=options or None)
|
|
34
|
+
|
|
35
|
+
def update_user_profile(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
36
|
+
return self._http.request("PATCH", f"/api/v1/services/customer-auth/users/{_q(id)}/profile", body=body, options=options or None)
|
|
37
|
+
|
|
38
|
+
def create_user_email_verification(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
39
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/email-verifications", body=body, options=options or None)
|
|
40
|
+
|
|
41
|
+
def complete_email_verification(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
42
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/email-verifications/complete", body=body, options=options or None)
|
|
43
|
+
|
|
44
|
+
def change_user_password(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
45
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/password", body=body, options=options or None)
|
|
46
|
+
|
|
47
|
+
def create_password_reset(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
48
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/password-resets", body=body, options=options or None)
|
|
49
|
+
|
|
50
|
+
def complete_password_reset(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
51
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/password-resets/complete", body=body, options=options or None)
|
|
52
|
+
|
|
53
|
+
def enrol_user_mfa(self, id: Any, **options: Any) -> Any:
|
|
54
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/mfa/enrol", options=options or None)
|
|
55
|
+
|
|
56
|
+
def confirm_user_mfa_enrol(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
57
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/mfa/enrol/confirm", body=body, options=options or None)
|
|
58
|
+
|
|
59
|
+
def disable_user_mfa(self, id: Any, **options: Any) -> Any:
|
|
60
|
+
return self._http.request("POST", f"/api/v1/services/customer-auth/users/{_q(id)}/mfa/disable", options=options or None)
|
|
61
|
+
|
|
62
|
+
def create_session(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
63
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/sessions", body=body, options=options or None)
|
|
64
|
+
|
|
65
|
+
def complete_session_mfa(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
66
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/sessions/mfa", body=body, options=options or None)
|
|
67
|
+
|
|
68
|
+
def revoke_session(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
69
|
+
return self._http.request("POST", "/api/v1/services/customer-auth/sessions/revoke", body=body, options=options or None)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FormsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def list_forms(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
12
|
+
return self._http.page("GET", "/api/v1/services/forms/forms", query=query, options=options or None)
|
|
13
|
+
|
|
14
|
+
def get_form_by_slug(self, slug: Any, **options: Any) -> Any:
|
|
15
|
+
return self._http.request("GET", f"/api/v1/services/forms/forms/by-slug/{_q(slug)}", options=options or None)
|
|
16
|
+
|
|
17
|
+
def get_form(self, id: Any, **options: Any) -> Any:
|
|
18
|
+
return self._http.request("GET", f"/api/v1/services/forms/forms/{_q(id)}", options=options or None)
|
|
19
|
+
|
|
20
|
+
def create_form(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("POST", "/api/v1/services/forms/forms", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def update_form(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
24
|
+
return self._http.request("PATCH", f"/api/v1/services/forms/forms/{_q(id)}", body=body, options=options or None)
|
|
25
|
+
|
|
26
|
+
def delete_form(self, id: Any, **options: Any) -> None:
|
|
27
|
+
self._http.request("DELETE", f"/api/v1/services/forms/forms/{_q(id)}", response_kind="none", options=options or None)
|
|
28
|
+
|
|
29
|
+
def list_form_submissions(self, id: Any, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
30
|
+
return self._http.page("GET", f"/api/v1/services/forms/forms/{_q(id)}/submissions", query=query, options=options or None)
|
|
31
|
+
|
|
32
|
+
def get_form_submission(self, id: Any, sub_id: Any, **options: Any) -> Any:
|
|
33
|
+
return self._http.request("GET", f"/api/v1/services/forms/forms/{_q(id)}/submissions/{_q(sub_id)}", options=options or None)
|
|
34
|
+
|
|
35
|
+
def update_form_submission(self, id: Any, sub_id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
36
|
+
return self._http.request("PATCH", f"/api/v1/services/forms/forms/{_q(id)}/submissions/{_q(sub_id)}", body=body, options=options or None)
|
|
37
|
+
|
|
38
|
+
def delete_form_submission(self, id: Any, sub_id: Any, **options: Any) -> None:
|
|
39
|
+
self._http.request("DELETE", f"/api/v1/services/forms/forms/{_q(id)}/submissions/{_q(sub_id)}", response_kind="none", options=options or None)
|
|
40
|
+
|
|
41
|
+
def download_form_submissions_csv(self, id: Any, **options: Any) -> str:
|
|
42
|
+
return self._http.request("GET", f"/api/v1/services/forms/forms/{_q(id)}/submissions.csv", response_kind="text", options=options or None)
|
|
43
|
+
|
|
44
|
+
def submit_form(self, slug: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
45
|
+
return self._http.request("POST", f"/api/v1/services/forms/forms/{_q(slug)}/submit", body=body, options=options or None)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InvoicingResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def list_customers(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
12
|
+
return self._http.page("GET", "/api/v1/services/invoicing/customers", query=query, options=options or None)
|
|
13
|
+
|
|
14
|
+
def get_customer(self, id: Any, **options: Any) -> Any:
|
|
15
|
+
return self._http.request("GET", f"/api/v1/services/invoicing/customers/{_q(id)}", options=options or None)
|
|
16
|
+
|
|
17
|
+
def create_customer(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
18
|
+
return self._http.request("POST", "/api/v1/services/invoicing/customers", body=body, options=options or None)
|
|
19
|
+
|
|
20
|
+
def update_customer(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("PUT", f"/api/v1/services/invoicing/customers/{_q(id)}", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def delete_customer(self, id: Any, **options: Any) -> Any:
|
|
24
|
+
return self._http.request("DELETE", f"/api/v1/services/invoicing/customers/{_q(id)}", options=options or None)
|
|
25
|
+
|
|
26
|
+
def create_customer_portal_token(self, id: Any, **options: Any) -> Any:
|
|
27
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/customers/{_q(id)}/portal-token", options=options or None)
|
|
28
|
+
|
|
29
|
+
def list_invoices(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
30
|
+
return self._http.page("GET", "/api/v1/services/invoicing/invoices", query=query, options=options or None)
|
|
31
|
+
|
|
32
|
+
def get_invoice(self, id: Any, **options: Any) -> Any:
|
|
33
|
+
return self._http.request("GET", f"/api/v1/services/invoicing/invoices/{_q(id)}", options=options or None)
|
|
34
|
+
|
|
35
|
+
def create_invoice(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
36
|
+
return self._http.request("POST", "/api/v1/services/invoicing/invoices", body=body, options=options or None)
|
|
37
|
+
|
|
38
|
+
def update_invoice(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
39
|
+
return self._http.request("PUT", f"/api/v1/services/invoicing/invoices/{_q(id)}", body=body, options=options or None)
|
|
40
|
+
|
|
41
|
+
def post_invoice(self, id: Any, **options: Any) -> Any:
|
|
42
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/invoices/{_q(id)}/post", options=options or None)
|
|
43
|
+
|
|
44
|
+
def send_invoice(self, id: Any, **options: Any) -> Any:
|
|
45
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/invoices/{_q(id)}/send", options=options or None)
|
|
46
|
+
|
|
47
|
+
def void_invoice(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
48
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/invoices/{_q(id)}/void", body=body, options=options or None)
|
|
49
|
+
|
|
50
|
+
def mark_invoice_paid(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
51
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/invoices/{_q(id)}/mark-paid", body=body, options=options or None)
|
|
52
|
+
|
|
53
|
+
def remind_invoice(self, id: Any, **options: Any) -> Any:
|
|
54
|
+
return self._http.request("POST", f"/api/v1/services/invoicing/invoices/{_q(id)}/remind", options=options or None)
|
|
55
|
+
|
|
56
|
+
def download_invoice_pdf(self, id: Any, **options: Any) -> bytes:
|
|
57
|
+
return self._http.request("GET", f"/api/v1/services/invoicing/invoices/{_q(id)}/pdf", response_kind="binary", options=options or None)
|
|
58
|
+
|
|
59
|
+
def list_recurring(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
60
|
+
return self._http.page("GET", "/api/v1/services/invoicing/recurring", query=query, options=options or None)
|
|
61
|
+
|
|
62
|
+
def create_recurring(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
63
|
+
return self._http.request("POST", "/api/v1/services/invoicing/recurring", body=body, options=options or None)
|
|
64
|
+
|
|
65
|
+
def create_credit_note(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
66
|
+
return self._http.request("POST", "/api/v1/services/invoicing/credit-notes", body=body, options=options or None)
|
|
67
|
+
|
|
68
|
+
def create_refund(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
69
|
+
return self._http.request("POST", "/api/v1/services/invoicing/refunds", body=body, options=options or None)
|
|
70
|
+
|
|
71
|
+
def get_approval(self, id: Any, **options: Any) -> Any:
|
|
72
|
+
return self._http.request("GET", f"/api/v1/services/invoicing/approvals/{_q(id)}", options=options or None)
|
|
73
|
+
|
|
74
|
+
def get_ar_aging_report(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Any:
|
|
75
|
+
return self._http.request("GET", "/api/v1/services/invoicing/reports/ar-aging", query=query, options=options or None)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaymentsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def create_checkout_session(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
12
|
+
return self._http.request("POST", "/api/v1/services/payments/checkout-sessions", body=body, options=options or None)
|
|
13
|
+
|
|
14
|
+
def list_payment_links(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
15
|
+
return self._http.page("GET", "/api/v1/services/payments/payment-links", query=query, options=options or None)
|
|
16
|
+
|
|
17
|
+
def get_payment_link(self, id: Any, **options: Any) -> Any:
|
|
18
|
+
return self._http.request("GET", f"/api/v1/services/payments/payment-links/{_q(id)}", options=options or None)
|
|
19
|
+
|
|
20
|
+
def create_payment_link(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("POST", "/api/v1/services/payments/payment-links", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def update_payment_link(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
24
|
+
return self._http.request("PATCH", f"/api/v1/services/payments/payment-links/{_q(id)}", body=body, options=options or None)
|
|
25
|
+
|
|
26
|
+
def delete_payment_link(self, id: Any, **options: Any) -> None:
|
|
27
|
+
self._http.request("DELETE", f"/api/v1/services/payments/payment-links/{_q(id)}", response_kind="none", options=options or None)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SmartChatResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def list_threads(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
12
|
+
return self._http.page("GET", "/api/v1/services/smart-chat/threads", query=query, options=options or None)
|
|
13
|
+
|
|
14
|
+
def create_thread(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
15
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/threads", body=body, options=options or None)
|
|
16
|
+
|
|
17
|
+
def get_thread(self, id: Any, **options: Any) -> Any:
|
|
18
|
+
return self._http.request("GET", f"/api/v1/services/smart-chat/threads/{_q(id)}", options=options or None)
|
|
19
|
+
|
|
20
|
+
def update_thread(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("PATCH", f"/api/v1/services/smart-chat/threads/{_q(id)}", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def delete_thread(self, id: Any, **options: Any) -> None:
|
|
24
|
+
self._http.request("DELETE", f"/api/v1/services/smart-chat/threads/{_q(id)}", response_kind="none", options=options or None)
|
|
25
|
+
|
|
26
|
+
def list_thread_messages(self, id: Any, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
27
|
+
return self._http.page("GET", f"/api/v1/services/smart-chat/threads/{_q(id)}/messages", query=query, options=options or None)
|
|
28
|
+
|
|
29
|
+
def create_message(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
30
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/messages", body=body, options=options or None)
|
|
31
|
+
|
|
32
|
+
def list_agents(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
33
|
+
return self._http.page("GET", "/api/v1/services/smart-chat/agents", query=query, options=options or None)
|
|
34
|
+
|
|
35
|
+
def get_agent(self, id: Any, **options: Any) -> Any:
|
|
36
|
+
return self._http.request("GET", f"/api/v1/services/smart-chat/agents/{_q(id)}", options=options or None)
|
|
37
|
+
|
|
38
|
+
def create_agent(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
39
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/agents", body=body, options=options or None)
|
|
40
|
+
|
|
41
|
+
def update_agent(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
42
|
+
return self._http.request("PUT", f"/api/v1/services/smart-chat/agents/{_q(id)}", body=body, options=options or None)
|
|
43
|
+
|
|
44
|
+
def delete_agent(self, id: Any, **options: Any) -> None:
|
|
45
|
+
self._http.request("DELETE", f"/api/v1/services/smart-chat/agents/{_q(id)}", response_kind="none", options=options or None)
|
|
46
|
+
|
|
47
|
+
def search_kb(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
48
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/kb/search", body=body, options=options or None)
|
|
49
|
+
|
|
50
|
+
def list_kb_documents(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
51
|
+
return self._http.page("GET", "/api/v1/services/smart-chat/kb/documents", query=query, options=options or None)
|
|
52
|
+
|
|
53
|
+
def get_kb_document(self, id: Any, **options: Any) -> Any:
|
|
54
|
+
return self._http.request("GET", f"/api/v1/services/smart-chat/kb/documents/{_q(id)}", options=options or None)
|
|
55
|
+
|
|
56
|
+
def delete_kb_document(self, id: Any, **options: Any) -> None:
|
|
57
|
+
self._http.request("DELETE", f"/api/v1/services/smart-chat/kb/documents/{_q(id)}", response_kind="none", options=options or None)
|
|
58
|
+
|
|
59
|
+
def import_kb_document(self, **options: Any) -> Any:
|
|
60
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/kb/documents/import", options=options or None)
|
|
61
|
+
|
|
62
|
+
def create_kb_documents_import_batch(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
63
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/kb/documents/import/batch", body=body, options=options or None)
|
|
64
|
+
|
|
65
|
+
def init_kb_documents_import(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
66
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/kb/documents/import/init", body=body, options=options or None)
|
|
67
|
+
|
|
68
|
+
def complete_kb_documents_import(self, task_id: Any, **options: Any) -> Any:
|
|
69
|
+
return self._http.request("POST", f"/api/v1/services/smart-chat/kb/documents/import/{_q(task_id)}/complete", options=options or None)
|
|
70
|
+
|
|
71
|
+
def list_kb_collections(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
72
|
+
return self._http.page("GET", "/api/v1/services/smart-chat/kb/collections", query=query, options=options or None)
|
|
73
|
+
|
|
74
|
+
def create_kb_collection(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
75
|
+
return self._http.request("POST", "/api/v1/services/smart-chat/kb/collections", body=body, options=options or None)
|
|
76
|
+
|
|
77
|
+
def get_task(self, task_id: Any, **options: Any) -> Any:
|
|
78
|
+
return self._http.request("GET", f"/api/v1/services/smart-chat/tasks/{_q(task_id)}", options=options or None)
|
|
79
|
+
|
|
80
|
+
def list_tasks(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
81
|
+
return self._http.page("GET", "/api/v1/services/smart-chat/tasks", query=query, options=options or None)
|
|
82
|
+
|
|
83
|
+
def cancel_task(self, task_id: Any, **options: Any) -> Any:
|
|
84
|
+
return self._http.request("POST", f"/api/v1/services/smart-chat/tasks/{_q(task_id)}/cancel", options=options or None)
|
|
85
|
+
|
|
86
|
+
def replay_task(self, task_id: Any, **options: Any) -> Any:
|
|
87
|
+
return self._http.request("POST", f"/api/v1/services/smart-chat/tasks/{_q(task_id)}/replay", options=options or None)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SocialResource:
|
|
7
|
+
def __init__(self, http: HttpClient) -> None:
|
|
8
|
+
self._http = http
|
|
9
|
+
|
|
10
|
+
def get_accounts(self, **options: Any) -> Any:
|
|
11
|
+
return self._http.request("GET", "/api/v1/services/social/accounts", options=options or None)
|
|
12
|
+
|
|
13
|
+
def get_posts(self, **options: Any) -> Any:
|
|
14
|
+
return self._http.request("GET", "/api/v1/services/social/posts", options=options or None)
|
|
15
|
+
|
|
16
|
+
def get_post(self, id: Any, **options: Any) -> Any:
|
|
17
|
+
return self._http.request("GET", f"/api/v1/services/social/posts/{_q(id)}", options=options or None)
|
|
18
|
+
|
|
19
|
+
def get_post_targets(self, id: Any, **options: Any) -> Any:
|
|
20
|
+
return self._http.request("GET", f"/api/v1/services/social/posts/{_q(id)}/targets", options=options or None)
|
|
21
|
+
|
|
22
|
+
def create_post(self, **options: Any) -> Any:
|
|
23
|
+
return self._http.request("POST", "/api/v1/services/social/posts", options=options or None)
|
|
24
|
+
|
|
25
|
+
def update_post(self, id: Any, **options: Any) -> Any:
|
|
26
|
+
return self._http.request("PATCH", f"/api/v1/services/social/posts/{_q(id)}", options=options or None)
|
|
27
|
+
|
|
28
|
+
def delete_post(self, id: Any, **options: Any) -> Any:
|
|
29
|
+
return self._http.request("DELETE", f"/api/v1/services/social/posts/{_q(id)}", options=options or None)
|
|
30
|
+
|
|
31
|
+
def publish_post(self, id: Any, **options: Any) -> Any:
|
|
32
|
+
return self._http.request("POST", f"/api/v1/services/social/posts/{_q(id)}/publish", options=options or None)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .._http import HttpClient, _q
|
|
4
|
+
from ..pagination import Page
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkflowsResource:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
def list_workflows(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
12
|
+
return self._http.page("GET", "/api/v1/services/workflows/workflows", query=query, options=options or None)
|
|
13
|
+
|
|
14
|
+
def get_workflow(self, id: Any, **options: Any) -> Any:
|
|
15
|
+
return self._http.request("GET", f"/api/v1/services/workflows/workflows/{_q(id)}", options=options or None)
|
|
16
|
+
|
|
17
|
+
def create_workflow(self, body: Dict[str, Any], **options: Any) -> Any:
|
|
18
|
+
return self._http.request("POST", "/api/v1/services/workflows/workflows", body=body, options=options or None)
|
|
19
|
+
|
|
20
|
+
def update_workflow(self, id: Any, body: Dict[str, Any], **options: Any) -> Any:
|
|
21
|
+
return self._http.request("PUT", f"/api/v1/services/workflows/workflows/{_q(id)}", body=body, options=options or None)
|
|
22
|
+
|
|
23
|
+
def delete_workflow(self, id: Any, **options: Any) -> None:
|
|
24
|
+
self._http.request("DELETE", f"/api/v1/services/workflows/workflows/{_q(id)}", response_kind="none", options=options or None)
|
|
25
|
+
|
|
26
|
+
def pause_workflow(self, id: Any, **options: Any) -> Any:
|
|
27
|
+
return self._http.request("POST", f"/api/v1/services/workflows/workflows/{_q(id)}/pause", options=options or None)
|
|
28
|
+
|
|
29
|
+
def resume_workflow(self, id: Any, **options: Any) -> Any:
|
|
30
|
+
return self._http.request("POST", f"/api/v1/services/workflows/workflows/{_q(id)}/resume", options=options or None)
|
|
31
|
+
|
|
32
|
+
def run_workflow(self, id: Any, **options: Any) -> Any:
|
|
33
|
+
return self._http.request("POST", f"/api/v1/services/workflows/workflows/{_q(id)}/run", options=options or None)
|
|
34
|
+
|
|
35
|
+
def get_run(self, id: Any, **options: Any) -> Any:
|
|
36
|
+
return self._http.request("GET", f"/api/v1/services/workflows/runs/{_q(id)}", options=options or None)
|
|
37
|
+
|
|
38
|
+
def list_runs(self, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
39
|
+
return self._http.page("GET", "/api/v1/services/workflows/runs", query=query, options=options or None)
|
|
40
|
+
|
|
41
|
+
def list_workflow_runs(self, id: Any, query: Optional[Dict[str, Any]] = None, **options: Any) -> Page:
|
|
42
|
+
return self._http.page("GET", f"/api/v1/services/workflows/workflows/{_q(id)}/runs", query=query, options=options or None)
|
|
43
|
+
|
|
44
|
+
def get_action_catalog(self, **options: Any) -> Any:
|
|
45
|
+
return self._http.request("GET", "/api/v1/services/workflows/action-catalog", options=options or None)
|
softsolz/webhooks.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .errors import WebhookSignatureError
|
|
7
|
+
|
|
8
|
+
DEFAULT_TOLERANCE_SECONDS = 300
|
|
9
|
+
_SIGNATURE_VERSION = "v1"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _parse_signature_header(header: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
13
|
+
if not isinstance(header, str) or not header:
|
|
14
|
+
return None
|
|
15
|
+
timestamp: Optional[int] = None
|
|
16
|
+
signatures: List[str] = []
|
|
17
|
+
for part in header.split(","):
|
|
18
|
+
if "=" not in part:
|
|
19
|
+
continue
|
|
20
|
+
key, _, value = part.partition("=")
|
|
21
|
+
key = key.strip()
|
|
22
|
+
value = value.strip()
|
|
23
|
+
if key == "t":
|
|
24
|
+
try:
|
|
25
|
+
timestamp = int(value)
|
|
26
|
+
except ValueError:
|
|
27
|
+
return None
|
|
28
|
+
elif key == _SIGNATURE_VERSION:
|
|
29
|
+
signatures.append(value)
|
|
30
|
+
if timestamp is None:
|
|
31
|
+
return None
|
|
32
|
+
return {"t": timestamp, "signatures": signatures}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compute_signature(timestamp: int, raw_body: Union[str, bytes], secret: str) -> str:
|
|
36
|
+
body_text = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else raw_body
|
|
37
|
+
payload = f"{timestamp}.{body_text}".encode("utf-8")
|
|
38
|
+
return hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def verify(
|
|
42
|
+
raw_body: Union[str, bytes],
|
|
43
|
+
header_value: Optional[str],
|
|
44
|
+
secret: str,
|
|
45
|
+
*,
|
|
46
|
+
tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
|
|
47
|
+
now: Optional[int] = None,
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
if not secret:
|
|
50
|
+
return {"valid": False, "reason": "missing_secret"}
|
|
51
|
+
parsed = _parse_signature_header(header_value)
|
|
52
|
+
if parsed is None:
|
|
53
|
+
return {"valid": False, "reason": "malformed_header"}
|
|
54
|
+
if not parsed["signatures"]:
|
|
55
|
+
return {"valid": False, "reason": "no_signatures"}
|
|
56
|
+
current = now if now is not None else int(time.time())
|
|
57
|
+
if abs(current - parsed["t"]) > tolerance_seconds:
|
|
58
|
+
return {"valid": False, "reason": "timestamp_outside_tolerance", "t": parsed["t"]}
|
|
59
|
+
expected = compute_signature(parsed["t"], raw_body, secret)
|
|
60
|
+
if any(hmac.compare_digest(sig, expected) for sig in parsed["signatures"]):
|
|
61
|
+
return {"valid": True, "t": parsed["t"]}
|
|
62
|
+
return {"valid": False, "reason": "signature_mismatch", "t": parsed["t"]}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def assert_valid(
|
|
66
|
+
raw_body: Union[str, bytes],
|
|
67
|
+
header_value: Optional[str],
|
|
68
|
+
secret: str,
|
|
69
|
+
*,
|
|
70
|
+
tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
|
|
71
|
+
now: Optional[int] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
result = verify(raw_body, header_value, secret, tolerance_seconds=tolerance_seconds, now=now)
|
|
74
|
+
if not result["valid"]:
|
|
75
|
+
raise WebhookSignatureError(result.get("reason", "invalid"))
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: softsolz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the SoftSolz platform API.
|
|
5
|
+
Project-URL: Homepage, https://developer.softsolz.uk
|
|
6
|
+
Project-URL: Documentation, https://developer.softsolz.uk
|
|
7
|
+
Project-URL: Repository, https://github.com/soft-solz/sdk
|
|
8
|
+
Author: SoftSolz
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: api,forms,invoicing,payments,sdk,softsolz,webhooks
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx<1,>=0.26
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# softsolz
|
|
27
|
+
|
|
28
|
+
Official Python SDK for the [SoftSolz platform API](https://developer.softsolz.uk).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install softsolz
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.9 or later. Fully type-annotated (`py.typed`).
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import os
|
|
40
|
+
from softsolz import SoftSolz
|
|
41
|
+
|
|
42
|
+
client = SoftSolz(api_key=os.environ["SOFTSOLZ_API_KEY"])
|
|
43
|
+
|
|
44
|
+
me = client.whoami()
|
|
45
|
+
|
|
46
|
+
form = client.forms.create_form({"name": "Contact Us", "status": "published"})
|
|
47
|
+
client.forms.submit_form(form["slug"], {"data": {"full_name": "Jane Doe", "email": "jane@example.com"}})
|
|
48
|
+
|
|
49
|
+
invoice = client.invoicing.create_invoice({
|
|
50
|
+
"customer_id": 42,
|
|
51
|
+
"lines": [{"description": "Design work", "quantity": 1, "unit_price_cents": 50000}],
|
|
52
|
+
})
|
|
53
|
+
client.invoicing.send_invoice(invoice["id"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use an `sk_test_*` key to run against your sandbox workspace; swap to `sk_live_*` in production. No other configuration changes.
|
|
57
|
+
|
|
58
|
+
### Services
|
|
59
|
+
|
|
60
|
+
`client.blogs`, `client.customer_auth`, `client.forms`, `client.invoicing`, `client.payments`, `client.smart_chat`, `client.social`, `client.workflows` - one method per API operation, generated from the platform's service manifests.
|
|
61
|
+
|
|
62
|
+
### Pagination
|
|
63
|
+
|
|
64
|
+
List methods return a `Page` you can iterate; extra pages are fetched automatically:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
for invoice in client.invoicing.list_invoices({"status": "overdue"}):
|
|
68
|
+
print(invoice["invoice_number"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or page manually with `page.data`, `page.has_more`, and `page.next_page()`.
|
|
72
|
+
|
|
73
|
+
### Errors
|
|
74
|
+
|
|
75
|
+
All API failures raise a typed subclass of `SoftSolzError` carrying `status`, `code`, `message`, `details`, and `request_id`:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from softsolz import NotFoundError
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
client.forms.get_form(999)
|
|
82
|
+
except NotFoundError as err:
|
|
83
|
+
print(err.code, err.request_id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Classes: `InvalidRequestError` (400/422), `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `retry_after_seconds`), `APIConnectionError` (network).
|
|
87
|
+
|
|
88
|
+
### Retries and idempotency
|
|
89
|
+
|
|
90
|
+
429 and 5xx responses are retried automatically with exponential backoff (default 2 retries), honouring `Retry-After`. Every mutating request carries an auto-generated `Idempotency-Key`, so retries are always safe. Pass your own to control replays:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
client.invoicing.create_invoice(body, idempotency_key="order-1234")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Configure per client: `SoftSolz(api_key=..., max_retries=3, timeout=60.0)`.
|
|
97
|
+
|
|
98
|
+
### Webhooks
|
|
99
|
+
|
|
100
|
+
Verify the `Softsolz-Signature` header on incoming webhooks using the raw request body:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from softsolz import webhooks
|
|
104
|
+
|
|
105
|
+
webhooks.assert_valid(
|
|
106
|
+
raw_body=request.get_data(),
|
|
107
|
+
header_value=request.headers.get("Softsolz-Signature"),
|
|
108
|
+
secret=os.environ["SOFTSOLZ_WEBHOOK_SECRET"],
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`webhooks.verify(...)` returns `{"valid": False, "reason": ...}` if you prefer not to raise.
|
|
113
|
+
|
|
114
|
+
### Escape hatch
|
|
115
|
+
|
|
116
|
+
Call any endpoint directly while keeping auth, retries, and errors:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
client.request("GET", "/api/v1/services/forms/forms", query={"limit": 10})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Documentation
|
|
123
|
+
|
|
124
|
+
- API reference: https://developer.softsolz.uk/api-reference.html
|
|
125
|
+
- Webhooks: https://developer.softsolz.uk/webhooks.html
|
|
126
|
+
- Playground: https://playground.softsolz.uk
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
softsolz/__init__.py,sha256=YD1NEWZcJ5JtPZn5wfwCiF0ylQlNbt7X_InW89s9t3g,760
|
|
2
|
+
softsolz/_client.py,sha256=6vKYq4gopvVnQm0BvvJcxDbvdhRBSWxHMLGRJHsvxvg,1701
|
|
3
|
+
softsolz/_http.py,sha256=ZjcH823skUgASN06uvniBXGb0_2zK3P4xm0N_BAW-QQ,5496
|
|
4
|
+
softsolz/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
5
|
+
softsolz/errors.py,sha256=D-L-5edehexpqx-6DdRzpijc5NUFWftYuov2JpYghfw,2622
|
|
6
|
+
softsolz/pagination.py,sha256=5NdpIiiR1hF7TpTmnbK33Zpku50G9GdmD94bDFFR9uI,1307
|
|
7
|
+
softsolz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
softsolz/webhooks.py,sha256=mvIOiE4ZXAHZe4xY6F2FYZoRxVQT0O1baeAMdegqSuQ,2623
|
|
9
|
+
softsolz/resources/__init__.py,sha256=oP11j7mNJPyMYxXy4Nyc0Aa_Z3GzS4BAHloNfwAaW_A,1033
|
|
10
|
+
softsolz/resources/blogs.py,sha256=aCGdPPk5wklzq22Jji_SzFIS5_P75mDbDkHz22M_oU4,2523
|
|
11
|
+
softsolz/resources/customer_auth.py,sha256=ohf9TfNGnR7svoS212NH_piDu07dmJlD9tzYaN3Q4AQ,4417
|
|
12
|
+
softsolz/resources/forms.py,sha256=FLKHwoTp4vd_CCY17isc1JZJnlUeg6_JxSA180FE-qw,2706
|
|
13
|
+
softsolz/resources/invoicing.py,sha256=iofUCXkJYdt2_MRx_YmqyrM6O_RH3BnkSkTGLcTuxcw,4587
|
|
14
|
+
softsolz/resources/payments.py,sha256=xpM50hCrtUjCUS6xrzMmJeZfYW-A9uWZvfnL8EzCsKs,1449
|
|
15
|
+
softsolz/resources/smart_chat.py,sha256=1xd68cZpdlAlAD99JTEK7vQqhYJ1-5qwo07xKbNXzSI,5439
|
|
16
|
+
softsolz/resources/social.py,sha256=7gvlYRsKFJi6rhkJX_G_j2zKIfOq-gd6x-ZfCcop1Jo,1498
|
|
17
|
+
softsolz/resources/workflows.py,sha256=ZH7D2e0SgaSykaWBPdLbgAdwJx4q2PM4wKuQXYXTlbU,2535
|
|
18
|
+
softsolz-0.1.0.dist-info/METADATA,sha256=A64Bv1rZWCb8MXONUosVrBViOWkykv6XfBt5DlHBF8k,4169
|
|
19
|
+
softsolz-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
20
|
+
softsolz-0.1.0.dist-info/RECORD,,
|