poelis-sdk 0.3.1__tar.gz → 0.3.2__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.
Potentially problematic release.
This version of poelis-sdk might be problematic. Click here for more details.
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/PKG-INFO +1 -1
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/pyproject.toml +1 -1
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/browser.py +74 -17
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/uv.lock +1 -1
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/.github/workflows/ci.yml +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/.github/workflows/codeql.yml +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/.github/workflows/publish-on-push.yml +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/.gitignore +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/LICENSE +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/README.md +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/notebooks/try_poelis_sdk.ipynb +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/__init__.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/_transport.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/client.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/exceptions.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/items.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/logging.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/models.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/org_validation.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/products.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/search.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/poelis_sdk/workspaces.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/tests/test_client_basic.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/tests/test_errors_and_backoff.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/tests/test_items_client.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/tests/test_search_client.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/src/tests/test_transport_and_products.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/tests/__init__.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/tests/test_browser_navigation.py +0 -0
- {poelis_sdk-0.3.1 → poelis_sdk-0.3.2}/tests/test_integration_smoke.py +0 -0
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
from types import MethodType
|
|
5
5
|
import re
|
|
6
|
+
import time
|
|
6
7
|
|
|
7
8
|
from .org_validation import get_organization_context_message
|
|
8
9
|
|
|
@@ -26,6 +27,10 @@ class _Node:
|
|
|
26
27
|
self._name = name
|
|
27
28
|
self._children_cache: Dict[str, "_Node"] = {}
|
|
28
29
|
self._props_cache: Optional[List[Dict[str, Any]]] = None
|
|
30
|
+
# Performance optimization: cache metadata with TTL
|
|
31
|
+
self._children_loaded_at: Optional[float] = None
|
|
32
|
+
self._props_loaded_at: Optional[float] = None
|
|
33
|
+
self._cache_ttl: float = 30.0 # 30 seconds cache TTL
|
|
29
34
|
|
|
30
35
|
def __repr__(self) -> str: # pragma: no cover - notebook UX
|
|
31
36
|
path = []
|
|
@@ -36,8 +41,9 @@ class _Node:
|
|
|
36
41
|
return f"<{self._level}:{'.'.join(reversed(path)) or '*'}>"
|
|
37
42
|
|
|
38
43
|
def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
|
|
39
|
-
#
|
|
40
|
-
self.
|
|
44
|
+
# Performance optimization: only load children if cache is stale or empty
|
|
45
|
+
if self._is_children_cache_stale():
|
|
46
|
+
self._load_children()
|
|
41
47
|
keys = list(self._children_cache.keys())
|
|
42
48
|
if self._level == "item":
|
|
43
49
|
# Include property names directly on item for suggestions
|
|
@@ -49,11 +55,30 @@ class _Node:
|
|
|
49
55
|
def _refresh(self) -> "_Node":
|
|
50
56
|
self._children_cache.clear()
|
|
51
57
|
self._props_cache = None
|
|
58
|
+
self._children_loaded_at = None
|
|
59
|
+
self._props_loaded_at = None
|
|
52
60
|
return self
|
|
53
61
|
|
|
62
|
+
def _is_children_cache_stale(self) -> bool:
|
|
63
|
+
"""Check if children cache is stale and needs refresh."""
|
|
64
|
+
if not self._children_cache:
|
|
65
|
+
return True
|
|
66
|
+
if self._children_loaded_at is None:
|
|
67
|
+
return True
|
|
68
|
+
return time.time() - self._children_loaded_at > self._cache_ttl
|
|
69
|
+
|
|
70
|
+
def _is_props_cache_stale(self) -> bool:
|
|
71
|
+
"""Check if properties cache is stale and needs refresh."""
|
|
72
|
+
if self._props_cache is None:
|
|
73
|
+
return True
|
|
74
|
+
if self._props_loaded_at is None:
|
|
75
|
+
return True
|
|
76
|
+
return time.time() - self._props_loaded_at > self._cache_ttl
|
|
77
|
+
|
|
54
78
|
def _names(self) -> List[str]:
|
|
55
79
|
"""Return display names of children at this level (internal)."""
|
|
56
|
-
self.
|
|
80
|
+
if self._is_children_cache_stale():
|
|
81
|
+
self._load_children()
|
|
57
82
|
return [child._name or "" for child in self._children_cache.values()]
|
|
58
83
|
|
|
59
84
|
def names(self) -> List[str]:
|
|
@@ -65,7 +90,8 @@ class _Node:
|
|
|
65
90
|
|
|
66
91
|
Only child keys are returned; for item level, property keys are also included.
|
|
67
92
|
"""
|
|
68
|
-
self.
|
|
93
|
+
if self._is_children_cache_stale():
|
|
94
|
+
self._load_children()
|
|
69
95
|
suggestions: List[str] = list(self._children_cache.keys())
|
|
70
96
|
if self._level == "item":
|
|
71
97
|
suggestions.extend(list(self._props_key_map().keys()))
|
|
@@ -78,7 +104,8 @@ class _Node:
|
|
|
78
104
|
raise AttributeError("props")
|
|
79
105
|
return _PropsNode(self)
|
|
80
106
|
if attr not in self._children_cache:
|
|
81
|
-
self.
|
|
107
|
+
if self._is_children_cache_stale():
|
|
108
|
+
self._load_children()
|
|
82
109
|
if attr in self._children_cache:
|
|
83
110
|
return self._children_cache[attr]
|
|
84
111
|
# Expose properties as direct attributes on item level
|
|
@@ -93,7 +120,8 @@ class _Node:
|
|
|
93
120
|
|
|
94
121
|
This enables names with spaces or symbols: browser["Workspace Name"].
|
|
95
122
|
"""
|
|
96
|
-
self.
|
|
123
|
+
if self._is_children_cache_stale():
|
|
124
|
+
self._load_children()
|
|
97
125
|
if key in self._children_cache:
|
|
98
126
|
return self._children_cache[key]
|
|
99
127
|
for child in self._children_cache.values():
|
|
@@ -105,10 +133,11 @@ class _Node:
|
|
|
105
133
|
raise KeyError(key)
|
|
106
134
|
|
|
107
135
|
def _properties(self) -> List[Dict[str, Any]]:
|
|
108
|
-
if self.
|
|
109
|
-
return self._props_cache
|
|
136
|
+
if not self._is_props_cache_stale():
|
|
137
|
+
return self._props_cache or []
|
|
110
138
|
if self._level != "item":
|
|
111
139
|
self._props_cache = []
|
|
140
|
+
self._props_loaded_at = time.time()
|
|
112
141
|
return self._props_cache
|
|
113
142
|
# Try direct properties(itemId: ...) first; fallback to searchProperties
|
|
114
143
|
q = (
|
|
@@ -128,6 +157,7 @@ class _Node:
|
|
|
128
157
|
if "errors" in data:
|
|
129
158
|
raise RuntimeError(data["errors"]) # trigger fallback
|
|
130
159
|
self._props_cache = data.get("data", {}).get("properties", []) or []
|
|
160
|
+
self._props_loaded_at = time.time()
|
|
131
161
|
except Exception:
|
|
132
162
|
q2 = (
|
|
133
163
|
"query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
|
|
@@ -142,6 +172,7 @@ class _Node:
|
|
|
142
172
|
if "errors" in data2:
|
|
143
173
|
raise RuntimeError(data2["errors"]) # propagate
|
|
144
174
|
self._props_cache = data2.get("data", {}).get("searchProperties", {}).get("hits", []) or []
|
|
175
|
+
self._props_loaded_at = time.time()
|
|
145
176
|
return self._props_cache
|
|
146
177
|
|
|
147
178
|
def _props_key_map(self) -> Dict[str, Dict[str, Any]]:
|
|
@@ -172,20 +203,26 @@ class _Node:
|
|
|
172
203
|
for w in rows:
|
|
173
204
|
display = w.get("name") or str(w.get("id"))
|
|
174
205
|
nm = _safe_key(display)
|
|
175
|
-
|
|
206
|
+
child = _Node(self._client, "workspace", self, w["id"], display)
|
|
207
|
+
child._cache_ttl = self._cache_ttl
|
|
208
|
+
self._children_cache[nm] = child
|
|
176
209
|
elif self._level == "workspace":
|
|
177
210
|
page = self._client.products.list_by_workspace(workspace_id=self._id, limit=200, offset=0)
|
|
178
211
|
for p in page.data:
|
|
179
212
|
display = p.name or str(p.id)
|
|
180
213
|
nm = _safe_key(display)
|
|
181
|
-
|
|
214
|
+
child = _Node(self._client, "product", self, p.id, display)
|
|
215
|
+
child._cache_ttl = self._cache_ttl
|
|
216
|
+
self._children_cache[nm] = child
|
|
182
217
|
elif self._level == "product":
|
|
183
218
|
rows = self._client.items.list_by_product(product_id=self._id, limit=1000, offset=0)
|
|
184
219
|
for it in rows:
|
|
185
220
|
if it.get("parentId") is None:
|
|
186
221
|
display = it.get("name") or str(it["id"])
|
|
187
222
|
nm = _safe_key(display)
|
|
188
|
-
|
|
223
|
+
child = _Node(self._client, "item", self, it["id"], display)
|
|
224
|
+
child._cache_ttl = self._cache_ttl
|
|
225
|
+
self._children_cache[nm] = child
|
|
189
226
|
elif self._level == "item":
|
|
190
227
|
# Fetch children items by parent; derive productId from ancestor product
|
|
191
228
|
anc = self
|
|
@@ -214,14 +251,27 @@ class _Node:
|
|
|
214
251
|
continue
|
|
215
252
|
display = it2.get("name") or str(it2["id"])
|
|
216
253
|
nm = _safe_key(display)
|
|
217
|
-
|
|
254
|
+
child = _Node(self._client, "item", self, it2["id"], display)
|
|
255
|
+
child._cache_ttl = self._cache_ttl
|
|
256
|
+
self._children_cache[nm] = child
|
|
257
|
+
|
|
258
|
+
# Mark cache as fresh
|
|
259
|
+
self._children_loaded_at = time.time()
|
|
218
260
|
|
|
219
261
|
|
|
220
262
|
class Browser:
|
|
221
263
|
"""Public browser entrypoint."""
|
|
222
264
|
|
|
223
|
-
def __init__(self, client: Any) -> None:
|
|
265
|
+
def __init__(self, client: Any, cache_ttl: float = 30.0) -> None:
|
|
266
|
+
"""Initialize browser with optional cache TTL.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
client: PoelisClient instance
|
|
270
|
+
cache_ttl: Cache time-to-live in seconds (default: 30)
|
|
271
|
+
"""
|
|
224
272
|
self._root = _Node(client, "root", None, None, None)
|
|
273
|
+
# Set cache TTL for all nodes
|
|
274
|
+
self._root._cache_ttl = cache_ttl
|
|
225
275
|
# Best-effort: auto-enable curated completion in interactive shells
|
|
226
276
|
global _AUTO_COMPLETER_INSTALLED
|
|
227
277
|
if not _AUTO_COMPLETER_INSTALLED:
|
|
@@ -245,8 +295,9 @@ class Browser:
|
|
|
245
295
|
return self._root[key]
|
|
246
296
|
|
|
247
297
|
def __dir__(self) -> list[str]: # pragma: no cover - notebook UX
|
|
248
|
-
#
|
|
249
|
-
self._root.
|
|
298
|
+
# Performance optimization: only load children if cache is stale or empty
|
|
299
|
+
if self._root._is_children_cache_stale():
|
|
300
|
+
self._root._load_children()
|
|
250
301
|
return sorted([*self._root._children_cache.keys()])
|
|
251
302
|
|
|
252
303
|
def _names(self) -> List[str]:
|
|
@@ -288,13 +339,18 @@ class _PropsNode:
|
|
|
288
339
|
self._item = item_node
|
|
289
340
|
self._children_cache: Dict[str, _PropWrapper] = {}
|
|
290
341
|
self._names: List[str] = []
|
|
342
|
+
self._loaded_at: Optional[float] = None
|
|
343
|
+
self._cache_ttl: float = item_node._cache_ttl # Inherit cache TTL from parent node
|
|
291
344
|
|
|
292
345
|
def __repr__(self) -> str: # pragma: no cover - notebook UX
|
|
293
346
|
return f"<props of {self._item.name or self._item.id}>"
|
|
294
347
|
|
|
295
348
|
def _ensure_loaded(self) -> None:
|
|
296
|
-
if
|
|
297
|
-
|
|
349
|
+
# Performance optimization: only load if cache is stale or empty
|
|
350
|
+
if self._children_cache and self._loaded_at is not None:
|
|
351
|
+
if time.time() - self._loaded_at <= self._cache_ttl:
|
|
352
|
+
return
|
|
353
|
+
|
|
298
354
|
props = self._item._properties()
|
|
299
355
|
used_names: Dict[str, int] = {}
|
|
300
356
|
names_list = []
|
|
@@ -313,6 +369,7 @@ class _PropsNode:
|
|
|
313
369
|
self._children_cache[safe] = _PropWrapper(pr)
|
|
314
370
|
names_list.append(display)
|
|
315
371
|
self._names = names_list
|
|
372
|
+
self._loaded_at = time.time()
|
|
316
373
|
|
|
317
374
|
def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
|
|
318
375
|
self._ensure_loaded()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|