softsolz 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ __pycache__/
5
+ *.egg-info/
6
+ .venv/
7
+ .pytest_cache/
8
+ coverage/
9
+ .DS_Store
@@ -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,105 @@
1
+ # softsolz
2
+
3
+ Official Python SDK for the [SoftSolz platform API](https://developer.softsolz.uk).
4
+
5
+ ```bash
6
+ pip install softsolz
7
+ ```
8
+
9
+ Requires Python 3.9 or later. Fully type-annotated (`py.typed`).
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ import os
15
+ from softsolz import SoftSolz
16
+
17
+ client = SoftSolz(api_key=os.environ["SOFTSOLZ_API_KEY"])
18
+
19
+ me = client.whoami()
20
+
21
+ form = client.forms.create_form({"name": "Contact Us", "status": "published"})
22
+ client.forms.submit_form(form["slug"], {"data": {"full_name": "Jane Doe", "email": "jane@example.com"}})
23
+
24
+ invoice = client.invoicing.create_invoice({
25
+ "customer_id": 42,
26
+ "lines": [{"description": "Design work", "quantity": 1, "unit_price_cents": 50000}],
27
+ })
28
+ client.invoicing.send_invoice(invoice["id"])
29
+ ```
30
+
31
+ Use an `sk_test_*` key to run against your sandbox workspace; swap to `sk_live_*` in production. No other configuration changes.
32
+
33
+ ### Services
34
+
35
+ `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.
36
+
37
+ ### Pagination
38
+
39
+ List methods return a `Page` you can iterate; extra pages are fetched automatically:
40
+
41
+ ```python
42
+ for invoice in client.invoicing.list_invoices({"status": "overdue"}):
43
+ print(invoice["invoice_number"])
44
+ ```
45
+
46
+ Or page manually with `page.data`, `page.has_more`, and `page.next_page()`.
47
+
48
+ ### Errors
49
+
50
+ All API failures raise a typed subclass of `SoftSolzError` carrying `status`, `code`, `message`, `details`, and `request_id`:
51
+
52
+ ```python
53
+ from softsolz import NotFoundError
54
+
55
+ try:
56
+ client.forms.get_form(999)
57
+ except NotFoundError as err:
58
+ print(err.code, err.request_id)
59
+ ```
60
+
61
+ Classes: `InvalidRequestError` (400/422), `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429, with `retry_after_seconds`), `APIConnectionError` (network).
62
+
63
+ ### Retries and idempotency
64
+
65
+ 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:
66
+
67
+ ```python
68
+ client.invoicing.create_invoice(body, idempotency_key="order-1234")
69
+ ```
70
+
71
+ Configure per client: `SoftSolz(api_key=..., max_retries=3, timeout=60.0)`.
72
+
73
+ ### Webhooks
74
+
75
+ Verify the `Softsolz-Signature` header on incoming webhooks using the raw request body:
76
+
77
+ ```python
78
+ from softsolz import webhooks
79
+
80
+ webhooks.assert_valid(
81
+ raw_body=request.get_data(),
82
+ header_value=request.headers.get("Softsolz-Signature"),
83
+ secret=os.environ["SOFTSOLZ_WEBHOOK_SECRET"],
84
+ )
85
+ ```
86
+
87
+ `webhooks.verify(...)` returns `{"valid": False, "reason": ...}` if you prefer not to raise.
88
+
89
+ ### Escape hatch
90
+
91
+ Call any endpoint directly while keeping auth, retries, and errors:
92
+
93
+ ```python
94
+ client.request("GET", "/api/v1/services/forms/forms", query={"limit": 10})
95
+ ```
96
+
97
+ ## Documentation
98
+
99
+ - API reference: https://developer.softsolz.uk/api-reference.html
100
+ - Webhooks: https://developer.softsolz.uk/webhooks.html
101
+ - Playground: https://playground.softsolz.uk
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "softsolz"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the SoftSolz platform API."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "SoftSolz" }]
13
+ keywords = ["softsolz", "sdk", "api", "forms", "invoicing", "payments", "webhooks"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ ]
25
+ dependencies = ["httpx>=0.26,<1"]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://developer.softsolz.uk"
32
+ Documentation = "https://developer.softsolz.uk"
33
+ Repository = "https://github.com/soft-solz/sdk"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/softsolz"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -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
+ ]
@@ -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()
@@ -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()
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -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)
@@ -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)
File without changes