poelis-sdk 0.3.1__py3-none-any.whl → 0.3.3__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/browser.py CHANGED
@@ -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,43 +133,84 @@ 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
- q = (
143
+ # Attempt 1: query with parsedValue support
144
+ q_parsed = (
115
145
  "query($iid: ID!) {\n"
116
146
  " properties(itemId: $iid) {\n"
117
147
  " __typename\n"
118
- " ... on NumericProperty { integerPart exponent category }\n"
119
- " ... on TextProperty { value }\n"
148
+ " ... on NumericProperty { category value parsedValue }\n"
149
+ " ... on TextProperty { value parsedValue }\n"
120
150
  " ... on DateProperty { value }\n"
121
151
  " }\n"
122
152
  "}"
123
153
  )
124
154
  try:
125
- r = self._client._transport.graphql(q, {"iid": self._id})
155
+ r = self._client._transport.graphql(q_parsed, {"iid": self._id})
126
156
  r.raise_for_status()
127
157
  data = r.json()
128
158
  if "errors" in data:
129
- raise RuntimeError(data["errors"]) # trigger fallback
159
+ raise RuntimeError(data["errors"]) # try value-only shape
130
160
  self._props_cache = data.get("data", {}).get("properties", []) or []
161
+ self._props_loaded_at = time.time()
131
162
  except Exception:
132
- q2 = (
133
- "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
134
- " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
135
- " hits { id workspaceId productId itemId propertyType name category value owner }\n"
163
+ # Attempt 2: value-only, legacy compatible
164
+ q_value_only = (
165
+ "query($iid: ID!) {\n"
166
+ " properties(itemId: $iid) {\n"
167
+ " __typename\n"
168
+ " ... on NumericProperty { category value }\n"
169
+ " ... on TextProperty { value }\n"
170
+ " ... on DateProperty { value }\n"
136
171
  " }\n"
137
172
  "}"
138
173
  )
139
- r2 = self._client._transport.graphql(q2, {"iid": self._id, "limit": 100, "offset": 0})
140
- r2.raise_for_status()
141
- data2 = r2.json()
142
- if "errors" in data2:
143
- raise RuntimeError(data2["errors"]) # propagate
144
- self._props_cache = data2.get("data", {}).get("searchProperties", {}).get("hits", []) or []
174
+ try:
175
+ r = self._client._transport.graphql(q_value_only, {"iid": self._id})
176
+ r.raise_for_status()
177
+ data = r.json()
178
+ if "errors" in data:
179
+ raise RuntimeError(data["errors"]) # trigger fallback to search
180
+ self._props_cache = data.get("data", {}).get("properties", []) or []
181
+ self._props_loaded_at = time.time()
182
+ except Exception:
183
+ # Fallback to searchProperties
184
+ q2_parsed = (
185
+ "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
186
+ " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
187
+ " hits { id workspaceId productId itemId propertyType name category value parsedValue owner }\n"
188
+ " }\n"
189
+ "}"
190
+ )
191
+ try:
192
+ r2 = self._client._transport.graphql(q2_parsed, {"iid": self._id, "limit": 100, "offset": 0})
193
+ r2.raise_for_status()
194
+ data2 = r2.json()
195
+ if "errors" in data2:
196
+ raise RuntimeError(data2["errors"]) # try minimal
197
+ self._props_cache = data2.get("data", {}).get("searchProperties", {}).get("hits", []) or []
198
+ self._props_loaded_at = time.time()
199
+ except Exception:
200
+ q2_min = (
201
+ "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
202
+ " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
203
+ " hits { id workspaceId productId itemId propertyType name category value owner }\n"
204
+ " }\n"
205
+ "}"
206
+ )
207
+ r3 = self._client._transport.graphql(q2_min, {"iid": self._id, "limit": 100, "offset": 0})
208
+ r3.raise_for_status()
209
+ data3 = r3.json()
210
+ if "errors" in data3:
211
+ raise RuntimeError(data3["errors"]) # propagate
212
+ self._props_cache = data3.get("data", {}).get("searchProperties", {}).get("hits", []) or []
213
+ self._props_loaded_at = time.time()
145
214
  return self._props_cache
146
215
 
147
216
  def _props_key_map(self) -> Dict[str, Dict[str, Any]]:
@@ -172,20 +241,26 @@ class _Node:
172
241
  for w in rows:
173
242
  display = w.get("name") or str(w.get("id"))
174
243
  nm = _safe_key(display)
175
- self._children_cache[nm] = _Node(self._client, "workspace", self, w["id"], display)
244
+ child = _Node(self._client, "workspace", self, w["id"], display)
245
+ child._cache_ttl = self._cache_ttl
246
+ self._children_cache[nm] = child
176
247
  elif self._level == "workspace":
177
248
  page = self._client.products.list_by_workspace(workspace_id=self._id, limit=200, offset=0)
178
249
  for p in page.data:
179
250
  display = p.name or str(p.id)
180
251
  nm = _safe_key(display)
181
- self._children_cache[nm] = _Node(self._client, "product", self, p.id, display)
252
+ child = _Node(self._client, "product", self, p.id, display)
253
+ child._cache_ttl = self._cache_ttl
254
+ self._children_cache[nm] = child
182
255
  elif self._level == "product":
183
256
  rows = self._client.items.list_by_product(product_id=self._id, limit=1000, offset=0)
184
257
  for it in rows:
185
258
  if it.get("parentId") is None:
186
259
  display = it.get("name") or str(it["id"])
187
260
  nm = _safe_key(display)
188
- self._children_cache[nm] = _Node(self._client, "item", self, it["id"], display)
261
+ child = _Node(self._client, "item", self, it["id"], display)
262
+ child._cache_ttl = self._cache_ttl
263
+ self._children_cache[nm] = child
189
264
  elif self._level == "item":
190
265
  # Fetch children items by parent; derive productId from ancestor product
191
266
  anc = self
@@ -214,14 +289,27 @@ class _Node:
214
289
  continue
215
290
  display = it2.get("name") or str(it2["id"])
216
291
  nm = _safe_key(display)
217
- self._children_cache[nm] = _Node(self._client, "item", self, it2["id"], display)
292
+ child = _Node(self._client, "item", self, it2["id"], display)
293
+ child._cache_ttl = self._cache_ttl
294
+ self._children_cache[nm] = child
295
+
296
+ # Mark cache as fresh
297
+ self._children_loaded_at = time.time()
218
298
 
219
299
 
220
300
  class Browser:
221
301
  """Public browser entrypoint."""
222
302
 
223
- def __init__(self, client: Any) -> None:
303
+ def __init__(self, client: Any, cache_ttl: float = 30.0) -> None:
304
+ """Initialize browser with optional cache TTL.
305
+
306
+ Args:
307
+ client: PoelisClient instance
308
+ cache_ttl: Cache time-to-live in seconds (default: 30)
309
+ """
224
310
  self._root = _Node(client, "root", None, None, None)
311
+ # Set cache TTL for all nodes
312
+ self._root._cache_ttl = cache_ttl
225
313
  # Best-effort: auto-enable curated completion in interactive shells
226
314
  global _AUTO_COMPLETER_INSTALLED
227
315
  if not _AUTO_COMPLETER_INSTALLED:
@@ -245,8 +333,9 @@ class Browser:
245
333
  return self._root[key]
246
334
 
247
335
  def __dir__(self) -> list[str]: # pragma: no cover - notebook UX
248
- # Ensure children are loaded so TAB shows options
249
- self._root._load_children()
336
+ # Performance optimization: only load children if cache is stale or empty
337
+ if self._root._is_children_cache_stale():
338
+ self._root._load_children()
250
339
  return sorted([*self._root._children_cache.keys()])
251
340
 
252
341
  def _names(self) -> List[str]:
@@ -288,13 +377,18 @@ class _PropsNode:
288
377
  self._item = item_node
289
378
  self._children_cache: Dict[str, _PropWrapper] = {}
290
379
  self._names: List[str] = []
380
+ self._loaded_at: Optional[float] = None
381
+ self._cache_ttl: float = item_node._cache_ttl # Inherit cache TTL from parent node
291
382
 
292
383
  def __repr__(self) -> str: # pragma: no cover - notebook UX
293
384
  return f"<props of {self._item.name or self._item.id}>"
294
385
 
295
386
  def _ensure_loaded(self) -> None:
296
- if self._children_cache:
297
- return
387
+ # Performance optimization: only load if cache is stale or empty
388
+ if self._children_cache and self._loaded_at is not None:
389
+ if time.time() - self._loaded_at <= self._cache_ttl:
390
+ return
391
+
298
392
  props = self._item._properties()
299
393
  used_names: Dict[str, int] = {}
300
394
  names_list = []
@@ -313,6 +407,7 @@ class _PropsNode:
313
407
  self._children_cache[safe] = _PropWrapper(pr)
314
408
  names_list.append(display)
315
409
  self._names = names_list
410
+ self._loaded_at = time.time()
316
411
 
317
412
  def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
318
413
  self._ensure_loaded()
@@ -362,6 +457,10 @@ class _PropWrapper:
362
457
  @property
363
458
  def value(self) -> Any: # type: ignore[override]
364
459
  p = self._raw
460
+ # Use parsedValue if available (new backend feature)
461
+ if "parsedValue" in p:
462
+ return p["parsedValue"]
463
+ # Fallback to legacy parsing logic for backward compatibility
365
464
  # searchProperties shape
366
465
  if "numericValue" in p and p.get("numericValue") is not None:
367
466
  return p["numericValue"]
poelis_sdk/models.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
3
+ from typing import Optional, Any, Union, List
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
@@ -25,3 +25,106 @@ class PaginatedProducts(BaseModel):
25
25
  offset: int
26
26
 
27
27
 
28
+ class PropertyValue(BaseModel):
29
+ """Base class for property values with typed access."""
30
+
31
+ raw_value: str = Field(alias="value")
32
+ parsed_value: Optional[Any] = Field(alias="parsedValue", default=None)
33
+
34
+ @property
35
+ def value(self) -> Any:
36
+ """Get the properly typed value, falling back to raw string if parsing failed."""
37
+ return self.parsed_value if self.parsed_value is not None else self.raw_value
38
+
39
+
40
+ class NumericProperty(BaseModel):
41
+ """Numeric property representation."""
42
+
43
+ id: str = Field(min_length=1)
44
+ item_id: str = Field(alias="itemId", min_length=1)
45
+ position: float
46
+ name: str = Field(min_length=1)
47
+ value: str
48
+ category: str
49
+ display_unit: Optional[str] = Field(alias="displayUnit", default=None)
50
+ owner: str = Field(min_length=1)
51
+ type: str = Field(min_length=1)
52
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
53
+
54
+ @property
55
+ def typed_value(self) -> Union[int, float, List[Any], str]:
56
+ """Get the properly typed value, falling back to raw string if parsing failed."""
57
+ return self.parsed_value if self.parsed_value is not None else self.value
58
+
59
+
60
+ class TextProperty(BaseModel):
61
+ """Text property representation."""
62
+
63
+ id: str = Field(min_length=1)
64
+ item_id: str = Field(alias="itemId", min_length=1)
65
+ position: float
66
+ name: str = Field(min_length=1)
67
+ value: str
68
+ owner: str = Field(min_length=1)
69
+ type: str = Field(min_length=1)
70
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
71
+
72
+ @property
73
+ def typed_value(self) -> Union[int, float, List[Any], str]:
74
+ """Get the properly typed value, falling back to raw string if parsing failed."""
75
+ return self.parsed_value if self.parsed_value is not None else self.value
76
+
77
+
78
+ class DateProperty(BaseModel):
79
+ """Date property representation."""
80
+
81
+ id: str = Field(min_length=1)
82
+ item_id: str = Field(alias="itemId", min_length=1)
83
+ position: float
84
+ name: str = Field(min_length=1)
85
+ value: str
86
+ owner: str = Field(min_length=1)
87
+ type: str = Field(min_length=1)
88
+ parsed_value: Optional[str] = Field(alias="parsedValue", default=None)
89
+
90
+ @property
91
+ def typed_value(self) -> str:
92
+ """Get the properly typed value, falling back to raw string if parsing failed."""
93
+ return self.parsed_value if self.parsed_value is not None else self.value
94
+
95
+
96
+ class PropertySearchResult(BaseModel):
97
+ """Property search result with unified fields across all property types."""
98
+
99
+ id: str = Field(min_length=1)
100
+ workspace_id: str = Field(alias="workspaceId", min_length=1)
101
+ product_id: str = Field(alias="productId", min_length=1)
102
+ item_id: str = Field(alias="itemId", min_length=1)
103
+ property_type: str = Field(alias="propertyType", min_length=1)
104
+ name: str = Field(min_length=1)
105
+ category: Optional[str] = None
106
+ display_unit: Optional[str] = Field(alias="displayUnit", default=None)
107
+ value: Any # Raw value from GraphQL
108
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
109
+ owner: str = Field(min_length=1)
110
+ created_by: str = Field(alias="createdBy", min_length=1)
111
+ created_at: str = Field(alias="createdAt", min_length=1)
112
+ updated_at: str = Field(alias="updatedAt", min_length=1)
113
+
114
+ @property
115
+ def typed_value(self) -> Union[int, float, List[Any], str]:
116
+ """Get the properly typed value, falling back to raw string if parsing failed."""
117
+ return self.parsed_value if self.parsed_value is not None else self.value
118
+
119
+
120
+ class PropertySearchResponse(BaseModel):
121
+ """Response for property search queries."""
122
+
123
+ query: str
124
+ hits: List[PropertySearchResult]
125
+ total: int
126
+ limit: int
127
+ offset: int
128
+ processing_time_ms: int = Field(alias="processingTimeMs")
129
+
130
+
poelis_sdk/search.py CHANGED
@@ -54,7 +54,7 @@ class SearchClient:
54
54
  "query($q: String!, $ws: ID, $pid: ID, $iid: ID, $ptype: String, $cat: String, $limit: Int!, $offset: Int!, $sort: String) {\n"
55
55
  " searchProperties(q: $q, workspaceId: $ws, productId: $pid, itemId: $iid, propertyType: $ptype, category: $cat, limit: $limit, offset: $offset, sort: $sort) {\n"
56
56
  " query total limit offset processingTimeMs\n"
57
- " hits { id workspaceId productId itemId propertyType name category value owner }\n"
57
+ " hits { id workspaceId productId itemId propertyType name category value parsedValue owner }\n"
58
58
  " }\n"
59
59
  "}"
60
60
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poelis-sdk
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -1,16 +1,16 @@
1
1
  poelis_sdk/__init__.py,sha256=vRKuvnMGtq2_6SYDPNpckSPYXTgMDD1vBAfZ1bXlHL0,924
2
2
  poelis_sdk/_transport.py,sha256=Na1neuS9JyLwHqhWkqwpQiGdbRtS0EJlWd587kQ88-s,7069
3
- poelis_sdk/browser.py,sha256=mmtG9WUCYmBLFCiElXPjXnKm_kWvPw6nvyakGr05heI,20393
3
+ poelis_sdk/browser.py,sha256=drWI86XKhVCcwOO27HD3gSRyLoJG13Ak5RZE0HC95WA,25183
4
4
  poelis_sdk/client.py,sha256=10__5po-foX36ZCCduQmzdoh9NNS320kyaqztUNtPvo,3872
5
5
  poelis_sdk/exceptions.py,sha256=qX5kpAr8ozJUOW-CNhmspWVIE-bvUZT_PUnimYuBxNY,1101
6
6
  poelis_sdk/items.py,sha256=vomXn43gcUlX2iUro3mpb8Qicmmt4sWFB2vXXxIfLsM,2575
7
7
  poelis_sdk/logging.py,sha256=zmg8Us-7qjDl0n_NfOSvDolLopy7Dc_hQ-pcrC63dY8,2442
8
- poelis_sdk/models.py,sha256=tpL7f66dsCobapKp3_rt-w6oiyYvWtoehLvCfjTDLl4,538
8
+ poelis_sdk/models.py,sha256=PduXuivhpJDjR_MmYYrZ1ha5IFPT8AW0jdm_wOrN2zA,4478
9
9
  poelis_sdk/org_validation.py,sha256=c4fB6ySTvcovWxG4F1wU_OBlP-FyuIaAUzCwqgJKzBE,5607
10
10
  poelis_sdk/products.py,sha256=bwV2mOPvBriy83F3BxWww1oSsyLZFQvh4XOiIE9fI1s,3240
11
- poelis_sdk/search.py,sha256=KQbdnu2khQDewddSJHR7JWysH1f2M7swK6MR-ZwrLAE,4101
11
+ poelis_sdk/search.py,sha256=3DqFd7ilTpizjOYgWplR7MTslnY89Q8AmUfn865TnKc,4113
12
12
  poelis_sdk/workspaces.py,sha256=hpmRl-Hswr4YDvObQdyVpegIYjUWno7A_BiVBz-AQGc,2383
13
- poelis_sdk-0.3.1.dist-info/METADATA,sha256=nqjn38g9SKvJ13LJk0aP0DgGy3y8txsAQVnbvxv_QQw,2968
14
- poelis_sdk-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- poelis_sdk-0.3.1.dist-info/licenses/LICENSE,sha256=EEmE_r8wk_pdXB8CWp1LG6sBOl7--hNSS2kV94cI6co,1075
16
- poelis_sdk-0.3.1.dist-info/RECORD,,
13
+ poelis_sdk-0.3.3.dist-info/METADATA,sha256=ipFrZcuCD5mRUbKJrpBnO7Ix6XozSYM4Nxh4tDIsXE8,2968
14
+ poelis_sdk-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ poelis_sdk-0.3.3.dist-info/licenses/LICENSE,sha256=EEmE_r8wk_pdXB8CWp1LG6sBOl7--hNSS2kV94cI6co,1075
16
+ poelis_sdk-0.3.3.dist-info/RECORD,,