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 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
+
@@ -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
+