poelis-sdk 0.3.0__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.

Files changed (30) hide show
  1. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/PKG-INFO +1 -1
  2. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/pyproject.toml +1 -1
  3. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/browser.py +74 -17
  4. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/client.py +3 -3
  5. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/uv.lock +1 -1
  6. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/.github/workflows/ci.yml +0 -0
  7. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/.github/workflows/codeql.yml +0 -0
  8. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/.github/workflows/publish-on-push.yml +0 -0
  9. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/.gitignore +0 -0
  10. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/LICENSE +0 -0
  11. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/README.md +0 -0
  12. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/notebooks/try_poelis_sdk.ipynb +0 -0
  13. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/__init__.py +0 -0
  14. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/_transport.py +0 -0
  15. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/exceptions.py +0 -0
  16. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/items.py +0 -0
  17. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/logging.py +0 -0
  18. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/models.py +0 -0
  19. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/org_validation.py +0 -0
  20. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/products.py +0 -0
  21. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/search.py +0 -0
  22. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/poelis_sdk/workspaces.py +0 -0
  23. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/tests/test_client_basic.py +0 -0
  24. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/tests/test_errors_and_backoff.py +0 -0
  25. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/tests/test_items_client.py +0 -0
  26. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/tests/test_search_client.py +0 -0
  27. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/src/tests/test_transport_and_products.py +0 -0
  28. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/tests/__init__.py +0 -0
  29. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/tests/test_browser_navigation.py +0 -0
  30. {poelis_sdk-0.3.0 → poelis_sdk-0.3.2}/tests/test_integration_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poelis-sdk
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Official Python SDK for Poelis
5
5
  Project-URL: Homepage, https://poelis.com
6
6
  Project-URL: Source, https://github.com/PoelisTechnologies/poelis-python-sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "poelis-sdk"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Official Python SDK for Poelis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- # Ensure children are loaded so TAB shows options immediately
40
- self._load_children()
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._load_children()
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._load_children()
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._load_children()
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._load_children()
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._props_cache is not None:
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
- self._children_cache[nm] = _Node(self._client, "workspace", self, w["id"], display)
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
- self._children_cache[nm] = _Node(self._client, "product", self, p.id, display)
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
- self._children_cache[nm] = _Node(self._client, "item", self, it["id"], display)
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
- self._children_cache[nm] = _Node(self._client, "item", self, it2["id"], display)
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
- # Ensure children are loaded so TAB shows options
249
- self._root._load_children()
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 self._children_cache:
297
- return
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()
@@ -31,7 +31,7 @@ class ClientConfig(BaseModel):
31
31
  timeout_seconds: Request timeout in seconds.
32
32
  """
33
33
 
34
- base_url: HttpUrl = Field(default="https://api.poelis.ai")
34
+ base_url: HttpUrl = Field(default="https://poelis-be-py-753618215333.europe-west1.run.app")
35
35
  api_key: str = Field(min_length=1)
36
36
  org_id: str = Field(min_length=1)
37
37
  timeout_seconds: float = 30.0
@@ -45,7 +45,7 @@ class PoelisClient:
45
45
  resource accessors to unblock incremental development.
46
46
  """
47
47
 
48
- def __init__(self, api_key: str, org_id: str, base_url: str = "https://api.poelis.ai", timeout_seconds: float = 30.0) -> None:
48
+ def __init__(self, api_key: str, org_id: str, base_url: str = "https://poelis-be-py-753618215333.europe-west1.run.app", timeout_seconds: float = 30.0) -> None:
49
49
  """Initialize the client with API endpoint and credentials.
50
50
 
51
51
  Args:
@@ -90,7 +90,7 @@ class PoelisClient:
90
90
  - POELIS_ORG_ID
91
91
  """
92
92
 
93
- base_url = os.environ.get("POELIS_BASE_URL", "https://api.poelis.ai")
93
+ base_url = os.environ.get("POELIS_BASE_URL", "https://poelis-be-py-753618215333.europe-west1.run.app")
94
94
  api_key = os.environ.get("POELIS_API_KEY")
95
95
  org_id = os.environ.get("POELIS_ORG_ID")
96
96
 
@@ -538,7 +538,7 @@ wheels = [
538
538
 
539
539
  [[package]]
540
540
  name = "poelis-sdk"
541
- version = "0.3.0"
541
+ version = "0.3.2"
542
542
  source = { editable = "." }
543
543
  dependencies = [
544
544
  { name = "build" },
File without changes
File without changes
File without changes
File without changes