poelis-sdk 0.5.4__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.
Potentially problematic release.
This version of poelis-sdk might be problematic. Click here for more details.
- poelis_sdk/__init__.py +30 -0
- poelis_sdk/_transport.py +147 -0
- poelis_sdk/browser.py +1998 -0
- poelis_sdk/change_tracker.py +769 -0
- poelis_sdk/client.py +204 -0
- poelis_sdk/exceptions.py +44 -0
- poelis_sdk/items.py +121 -0
- poelis_sdk/logging.py +73 -0
- poelis_sdk/models.py +183 -0
- poelis_sdk/org_validation.py +163 -0
- poelis_sdk/products.py +167 -0
- poelis_sdk/search.py +88 -0
- poelis_sdk/versions.py +123 -0
- poelis_sdk/workspaces.py +50 -0
- poelis_sdk-0.5.4.dist-info/METADATA +113 -0
- poelis_sdk-0.5.4.dist-info/RECORD +18 -0
- poelis_sdk-0.5.4.dist-info/WHEEL +4 -0
- poelis_sdk-0.5.4.dist-info/licenses/LICENSE +21 -0
poelis_sdk/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Poelis Python SDK public exports.
|
|
2
|
+
|
|
3
|
+
Exposes the primary client and resolves the package version from installed
|
|
4
|
+
metadata so it stays in sync with ``pyproject.toml`` without manual edits.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import metadata
|
|
8
|
+
|
|
9
|
+
from .client import PoelisClient
|
|
10
|
+
from .logging import configure_logging, debug_logging, get_logger, quiet_logging, verbose_logging
|
|
11
|
+
|
|
12
|
+
__all__ = ["PoelisClient", "__version__", "configure_logging", "quiet_logging", "verbose_logging", "debug_logging", "get_logger"]
|
|
13
|
+
|
|
14
|
+
def _resolve_version() -> str:
|
|
15
|
+
"""Return installed package version or a dev fallback.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: Version string from package metadata, or ``"0.0.0-dev"`` when
|
|
19
|
+
metadata is unavailable (e.g., editable installs without built metadata).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
return metadata.version("poelis-sdk")
|
|
24
|
+
except metadata.PackageNotFoundError:
|
|
25
|
+
return "0.0.0-dev"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__version__: str = _resolve_version()
|
|
29
|
+
|
|
30
|
+
|
poelis_sdk/_transport.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
ClientError,
|
|
11
|
+
HTTPError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
UnauthorizedError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
"""HTTP transport abstraction for the Poelis SDK.
|
|
19
|
+
|
|
20
|
+
Provides a thin wrapper around httpx with sensible defaults for timeouts,
|
|
21
|
+
retries, and headers including authentication and optional org scoping.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Transport:
|
|
26
|
+
"""Synchronous HTTP transport using httpx.Client.
|
|
27
|
+
|
|
28
|
+
This wrapper centralizes auth headers, tenant scoping, timeouts, and
|
|
29
|
+
retry behavior. Retries are implemented here in a simple, explicit way to
|
|
30
|
+
avoid external dependencies, following the professional defaults defined
|
|
31
|
+
in the SDK planning document.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, base_url: str, api_key: str, timeout_seconds: float) -> None:
|
|
35
|
+
"""Initialize the transport.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Base API URL.
|
|
39
|
+
api_key: API key provided by backend to authenticate requests.
|
|
40
|
+
timeout_seconds: Request timeout in seconds.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self._client = httpx.Client(base_url=base_url, timeout=timeout_seconds)
|
|
44
|
+
self._api_key = api_key
|
|
45
|
+
self._timeout = timeout_seconds
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _headers(self, extra: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
|
49
|
+
headers: Dict[str, str] = {
|
|
50
|
+
"Accept": "application/json",
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
}
|
|
53
|
+
# Always send API key as Bearer token; backend derives org/workspace from key.
|
|
54
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
55
|
+
if extra:
|
|
56
|
+
headers.update(dict(extra))
|
|
57
|
+
return headers
|
|
58
|
+
|
|
59
|
+
def get(self, path: str, params: Optional[Mapping[str, Any]] = None) -> httpx.Response:
|
|
60
|
+
return self._request("GET", path, params=params)
|
|
61
|
+
|
|
62
|
+
def post(self, path: str, json: Any = None) -> httpx.Response: # noqa: A003
|
|
63
|
+
return self._request("POST", path, json=json)
|
|
64
|
+
|
|
65
|
+
def patch(self, path: str, json: Any = None) -> httpx.Response:
|
|
66
|
+
return self._request("PATCH", path, json=json)
|
|
67
|
+
|
|
68
|
+
def delete(self, path: str) -> httpx.Response:
|
|
69
|
+
return self._request("DELETE", path)
|
|
70
|
+
|
|
71
|
+
def graphql(self, query: str, variables: Optional[Mapping[str, Any]] = None) -> httpx.Response:
|
|
72
|
+
"""Post a GraphQL operation to /v1/graphql.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
query: GraphQL document string.
|
|
76
|
+
variables: Optional mapping of variables.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
payload: Dict[str, Any] = {"query": query, "variables": dict(variables or {})}
|
|
80
|
+
return self._request("POST", "/v1/graphql", json=payload)
|
|
81
|
+
|
|
82
|
+
def _request(self, method: str, path: str, *, params: Optional[Mapping[str, Any]] = None, json: Any = None) -> httpx.Response:
|
|
83
|
+
# Retries: up to 3 attempts on idempotent GET/HEAD and 429s respecting Retry-After.
|
|
84
|
+
max_attempts = 3
|
|
85
|
+
last_exc: Optional[Exception] = None
|
|
86
|
+
for attempt in range(1, max_attempts + 1):
|
|
87
|
+
try:
|
|
88
|
+
response = self._client.request(method, path, headers=self._headers(), params=params, json=json)
|
|
89
|
+
# Map common error codes
|
|
90
|
+
if 200 <= response.status_code < 300:
|
|
91
|
+
return response
|
|
92
|
+
if response.status_code == 401:
|
|
93
|
+
raise UnauthorizedError(401, message=_safe_message(response))
|
|
94
|
+
if response.status_code == 404:
|
|
95
|
+
raise NotFoundError(404, message=_safe_message(response))
|
|
96
|
+
if response.status_code == 429:
|
|
97
|
+
retry_after_header = response.headers.get("Retry-After")
|
|
98
|
+
retry_after: Optional[float] = None
|
|
99
|
+
if retry_after_header:
|
|
100
|
+
try:
|
|
101
|
+
retry_after = float(retry_after_header)
|
|
102
|
+
except Exception:
|
|
103
|
+
retry_after = None
|
|
104
|
+
if attempt < max_attempts:
|
|
105
|
+
if retry_after is not None:
|
|
106
|
+
time.sleep(retry_after)
|
|
107
|
+
else:
|
|
108
|
+
# fallback exponential backoff with jitter
|
|
109
|
+
time.sleep(_backoff_sleep(attempt))
|
|
110
|
+
continue
|
|
111
|
+
raise RateLimitError(429, message=_safe_message(response), retry_after_seconds=retry_after)
|
|
112
|
+
if 400 <= response.status_code < 500:
|
|
113
|
+
raise ClientError(response.status_code, message=_safe_message(response))
|
|
114
|
+
if 500 <= response.status_code < 600:
|
|
115
|
+
# Retry on idempotent
|
|
116
|
+
if method in {"GET", "HEAD"} and attempt < max_attempts:
|
|
117
|
+
time.sleep(_backoff_sleep(attempt))
|
|
118
|
+
continue
|
|
119
|
+
raise ServerError(response.status_code, message=_safe_message(response))
|
|
120
|
+
# Fallback
|
|
121
|
+
raise HTTPError(response.status_code, message=_safe_message(response))
|
|
122
|
+
except httpx.HTTPError as exc:
|
|
123
|
+
last_exc = exc
|
|
124
|
+
if method not in {"GET", "HEAD"} or attempt == max_attempts:
|
|
125
|
+
raise
|
|
126
|
+
assert last_exc is not None
|
|
127
|
+
raise last_exc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _safe_message(response: httpx.Response) -> str:
|
|
131
|
+
try:
|
|
132
|
+
data = response.json()
|
|
133
|
+
if isinstance(data, dict):
|
|
134
|
+
msg = data.get("message") or data.get("detail") or data.get("error")
|
|
135
|
+
if isinstance(msg, str):
|
|
136
|
+
return msg
|
|
137
|
+
return response.text
|
|
138
|
+
except Exception:
|
|
139
|
+
return response.text
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _backoff_sleep(attempt: int) -> float:
|
|
143
|
+
# Exponential backoff with jitter: base 0.5s, cap ~4s
|
|
144
|
+
base = 0.5 * (2 ** (attempt - 1))
|
|
145
|
+
return min(4.0, base + random.uniform(0, 0.25))
|
|
146
|
+
|
|
147
|
+
|