poelis-sdk 0.1.9__tar.gz → 0.2.0__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 (32) hide show
  1. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/PKG-INFO +1 -18
  2. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/README.md +0 -17
  3. poelis_sdk-0.2.0/notebooks/try_poelis_sdk.ipynb +220 -0
  4. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/pyproject.toml +1 -1
  5. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/browser.py +171 -28
  6. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/items.py +2 -2
  7. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/search.py +3 -3
  8. poelis_sdk-0.2.0/tests/test_browser_navigation.py +130 -0
  9. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/uv.lock +1 -1
  10. poelis_sdk-0.1.9/notebooks/try_poelis_sdk.ipynb +0 -587
  11. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/.github/workflows/ci.yml +0 -0
  12. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/.github/workflows/codeql.yml +0 -0
  13. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/.github/workflows/publish-on-push.yml +0 -0
  14. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/.gitignore +0 -0
  15. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/LICENSE +0 -0
  16. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/__init__.py +0 -0
  17. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/_transport.py +0 -0
  18. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/auth0.py +0 -0
  19. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/client.py +0 -0
  20. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/exceptions.py +0 -0
  21. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/logging.py +0 -0
  22. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/models.py +0 -0
  23. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/org_validation.py +0 -0
  24. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/products.py +0 -0
  25. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/poelis_sdk/workspaces.py +0 -0
  26. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/tests/test_client_basic.py +0 -0
  27. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/tests/test_errors_and_backoff.py +0 -0
  28. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/tests/test_items_client.py +0 -0
  29. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/tests/test_search_client.py +0 -0
  30. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/src/tests/test_transport_and_products.py +0 -0
  31. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/tests/__init__.py +0 -0
  32. {poelis_sdk-0.1.9 → poelis_sdk-0.2.0}/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.1.9
3
+ Version: 0.2.0
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
@@ -42,22 +42,6 @@ client = PoelisClient(
42
42
  api_key="poelis_live_A1B2C3...", # Organization Settings → API Keys
43
43
  org_id="tenant_uci_001", # same section
44
44
  )
45
-
46
- # Workspaces → Products
47
- workspaces = client.workspaces.list(limit=10, offset=0)
48
- ws_id = workspaces[0]["id"]
49
-
50
- page = client.products.list_by_workspace(workspace_id=ws_id, limit=10, offset=0)
51
- print([p.name for p in page.data])
52
-
53
- # Items for a product
54
- pid = page.data[0].id
55
- items = client.items.list_by_product(product_id=pid, limit=10, offset=0)
56
- print([i.get("name") for i in items])
57
-
58
- # Property search
59
- props = client.search.properties(q="*", workspace_id=ws_id, limit=10, offset=0)
60
- print(props["total"], len(props["hits"]))
61
45
  ```
62
46
 
63
47
  ## Configuration
@@ -73,7 +57,6 @@ print(props["total"], len(props["hits"]))
73
57
  ```bash
74
58
  export POELIS_API_KEY=poelis_live_A1B2C3...
75
59
  export POELIS_ORG_ID=tenant_id_001
76
- # POELIS_BASE_URL is optional - defaults to the managed GCP endpoint
77
60
  ```
78
61
 
79
62
 
@@ -19,22 +19,6 @@ client = PoelisClient(
19
19
  api_key="poelis_live_A1B2C3...", # Organization Settings → API Keys
20
20
  org_id="tenant_uci_001", # same section
21
21
  )
22
-
23
- # Workspaces → Products
24
- workspaces = client.workspaces.list(limit=10, offset=0)
25
- ws_id = workspaces[0]["id"]
26
-
27
- page = client.products.list_by_workspace(workspace_id=ws_id, limit=10, offset=0)
28
- print([p.name for p in page.data])
29
-
30
- # Items for a product
31
- pid = page.data[0].id
32
- items = client.items.list_by_product(product_id=pid, limit=10, offset=0)
33
- print([i.get("name") for i in items])
34
-
35
- # Property search
36
- props = client.search.properties(q="*", workspace_id=ws_id, limit=10, offset=0)
37
- print(props["total"], len(props["hits"]))
38
22
  ```
39
23
 
40
24
  ## Configuration
@@ -50,7 +34,6 @@ print(props["total"], len(props["hits"]))
50
34
  ```bash
51
35
  export POELIS_API_KEY=poelis_live_A1B2C3...
52
36
  export POELIS_ORG_ID=tenant_id_001
53
- # POELIS_BASE_URL is optional - defaults to the managed GCP endpoint
54
37
  ```
55
38
 
56
39
 
@@ -0,0 +1,220 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Poelis Python SDK Tutorial\n",
8
+ "\n",
9
+ "Welcome to the Poelis Python SDK tutorial! This notebook will guide you through setting up and using the SDK to interact with the Poelis API.\n",
10
+ "\n",
11
+ "## What is Poelis?\n",
12
+ "\n",
13
+ "Poelis is a platform for managing hierarchical data structures, products, items, and their properties. The Python SDK provides a convenient way to interact with the Poelis API from your Python applications.\n",
14
+ "\n",
15
+ "## Table of Contents\n",
16
+ "\n",
17
+ "1. [Installation](#Installation)\n",
18
+ "2. [Authentication Setup](#Authentication-Setup)\n",
19
+ "3. [Basic Client Setup](#Basic-Client-Setup)\n",
20
+ "4. [Browser Interface](#Browser-Interface)\n",
21
+ "\n"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "markdown",
26
+ "metadata": {},
27
+ "source": [
28
+ "## 1. Installation\n",
29
+ "\n",
30
+ "First, let's install the Poelis Python SDK. The SDK requires Python 3.11 or higher.\n",
31
+ "\n",
32
+ "```bash\n",
33
+ "pip install -U poelis-sdk\n",
34
+ "```\n",
35
+ "\n",
36
+ "### Requirements\n",
37
+ "- Python >= 3.11\n",
38
+ "- Access to a Poelis API endpoint\n",
39
+ "- Valid API credentials (API key + organization ID)\n"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "metadata": {},
46
+ "outputs": [],
47
+ "source": [
48
+ "!pip install -U poelis-sdk"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": null,
54
+ "metadata": {},
55
+ "outputs": [],
56
+ "source": [
57
+ "# Import the SDK\n",
58
+ "from poelis_sdk import PoelisClient\n",
59
+ "\n",
60
+ "# Check if the SDK is properly installed\n",
61
+ "print(\"Poelis SDK imported successfully!\")\n",
62
+ "print(f\"PoelisClient class: {PoelisClient}\")\n"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "markdown",
67
+ "metadata": {},
68
+ "source": [
69
+ "## 2. Authentication Setup\n",
70
+ "\n",
71
+ "Before you can use the SDK, you need to obtain your API credentials from the Poelis web interface.\n",
72
+ "\n",
73
+ "### Getting Your Credentials\n",
74
+ "\n",
75
+ "1. **Navigate to Organization Settings → API Keys** in the Poelis web interface\n",
76
+ "2. **Create a new API key**:\n",
77
+ " - Choose a descriptive name\n",
78
+ " - Select appropriate scopes\n",
79
+ " - Copy the key immediately\n",
80
+ "3. **Note your Organization ID** (displayed in the same section)\n",
81
+ "\n",
82
+ "### Environment Variables (Recommended)\n",
83
+ "\n",
84
+ "For security, store your credentials as environment variables:\n",
85
+ "\n",
86
+ "```bash\n",
87
+ "export POELIS_API_KEY=poelis_live_A1B2C3...\n",
88
+ "export POELIS_ORG_ID=org_id_001\n",
89
+ "```\n"
90
+ ]
91
+ },
92
+ {
93
+ "cell_type": "markdown",
94
+ "metadata": {},
95
+ "source": [
96
+ "## 3. Basic Client Setup\n",
97
+ "\n",
98
+ "There are two ways to initialize the Poelis client:\n",
99
+ "\n",
100
+ "### Method 1: Direct Initialization (Simplified)\n",
101
+ "\n",
102
+ "```python\n",
103
+ "# Simple usage with defaults\n",
104
+ "client = PoelisClient(\n",
105
+ " api_key=\"poelis_live_A1B2C3...\",\n",
106
+ " org_id=\"tenant_id_001\"\n",
107
+ ")\n",
108
+ "```\n",
109
+ "\n",
110
+ "### Method 2: From Environment Variables (Recommended)\n",
111
+ "\n",
112
+ "```python\n",
113
+ "client = PoelisClient.from_env()\n",
114
+ "```\n",
115
+ "\n",
116
+ "This method automatically reads the `POELIS_API_KEY` and `POELIS_ORG_ID` environment variables.\n"
117
+ ]
118
+ },
119
+ {
120
+ "cell_type": "code",
121
+ "execution_count": null,
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": [
125
+ "# Example: Initialize client (replace with your actual credentials)\n",
126
+ "# For this demo, we'll use placeholder values\n",
127
+ "# In practice, use environment variables or replace with your actual credentials\n",
128
+ "\n",
129
+ "try:\n",
130
+ " # Try to initialize from environment variables first\n",
131
+ " client = PoelisClient.from_env()\n",
132
+ " print(\"✅ Client initialized from environment variables\")\n",
133
+ " print(f\"Base URL: {client.base_url}\")\n",
134
+ " print(f\"Organization ID: {client.org_id}\")\n",
135
+ "except ValueError as e:\n",
136
+ " print(f\"❌ Environment variables not set: {e}\")\n",
137
+ " print(\"Please set POELIS_API_KEY and POELIS_ORG_ID (POELIS_BASE_URL is optional)\")\n",
138
+ " \n",
139
+ "# Fallback to manual initialization (replace with your credentials)\n",
140
+ "client = PoelisClient(\n",
141
+ " api_key=\"your_api_key_here\",\n",
142
+ " org_id=\"your_org_id_here\"\n",
143
+ ")\n"
144
+ ]
145
+ },
146
+ {
147
+ "cell_type": "markdown",
148
+ "metadata": {},
149
+ "source": [
150
+ "## 4. Browser Interface - Interactive Exploration\n",
151
+ "\n",
152
+ "The Poelis SDK provides a powerful **browser interface** for interactive exploration of your data hierarchy. This is often the best way to start exploring your data!\n",
153
+ "\n",
154
+ "### What is the Browser Interface?\n",
155
+ "\n",
156
+ "The browser interface allows you to navigate through your data using **dot notation** and **tab completion**:\n",
157
+ "- `client.browser` - Start here\n",
158
+ "- `client.browser.<workspace>` - Access a specific workspace\n",
159
+ "- `client.browser.<workspace>.<product>` - Access a specific product\n",
160
+ "- `client.browser.<workspace>.<product>.<item>` - Access a specific item\n",
161
+ "- And so on...\n",
162
+ "\n"
163
+ ]
164
+ },
165
+ {
166
+ "cell_type": "code",
167
+ "execution_count": null,
168
+ "metadata": {},
169
+ "outputs": [],
170
+ "source": [
171
+ "from poelis_sdk import PoelisClient\n",
172
+ "\n",
173
+ "# 1) Create the client\n",
174
+ "client = PoelisClient(\n",
175
+ " api_key=\"YOUR_API_KEY\",\n",
176
+ " org_id=\"YOUR_ORG_ID\",\n",
177
+ ")\n",
178
+ "\n",
179
+ "# 2) Explore with TAB (completion is installed automatically when accessing client.browser):\n",
180
+ "# - Type: client.browser. and press TAB to see workspaces\n",
181
+ "# - Then: client.browser.<workspace>. and TAB to see products\n",
182
+ "# - Then: client.browser.<workspace>.<product>. and TAB to see items\n",
183
+ "# - On an item, TAB shows child items and property keys\n",
184
+ "\n",
185
+ "# 3) Access a property’s value and category via dot paths\n",
186
+ "# Example (replace names with your actual names shown by TAB):\n",
187
+ "workspace = client.browser.UH2_Workspace\n",
188
+ "val = workspace.LH2_Tank.cold_box.clv.mass.value\n",
189
+ "cat = workspace.LH2_Tank.cold_box.clv.mass.category\n",
190
+ "\n",
191
+ "print(\"property value:\", val)\n",
192
+ "print(\"property category:\", cat)\n",
193
+ "\n",
194
+ "# 4) Go deeper into child items if needed (TAB will suggest only item/property names)\n",
195
+ "inner = client.browser.UH2_Workspace.LH2_Tank.inner_tank"
196
+ ]
197
+ }
198
+ ],
199
+ "metadata": {
200
+ "kernelspec": {
201
+ "display_name": "Python 3",
202
+ "language": "python",
203
+ "name": "python3"
204
+ },
205
+ "language_info": {
206
+ "codemirror_mode": {
207
+ "name": "ipython",
208
+ "version": 3
209
+ },
210
+ "file_extension": ".py",
211
+ "mimetype": "text/x-python",
212
+ "name": "python",
213
+ "nbconvert_exporter": "python",
214
+ "pygments_lexer": "ipython3",
215
+ "version": "3.11.13"
216
+ }
217
+ },
218
+ "nbformat": 4,
219
+ "nbformat_minor": 2
220
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "poelis-sdk"
7
- version = "0.1.9"
7
+ version = "0.2.0"
8
8
  description = "Official Python SDK for Poelis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import Any, Dict, List, Optional
4
+ from types import MethodType
4
5
  import re
5
6
 
6
7
  from .org_validation import get_organization_context_message
@@ -12,6 +13,10 @@ with optional property listing on items. Designed for notebook UX.
12
13
  """
13
14
 
14
15
 
16
+ # Internal guard to avoid repeated completer installation
17
+ _AUTO_COMPLETER_INSTALLED: bool = False
18
+
19
+
15
20
  class _Node:
16
21
  def __init__(self, client: Any, level: str, parent: Optional["_Node"], node_id: Optional[str], name: Optional[str]) -> None:
17
22
  self._client = client
@@ -33,34 +38,37 @@ class _Node:
33
38
  def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
34
39
  # Ensure children are loaded so TAB shows options immediately
35
40
  self._load_children()
36
- keys = list(self._children_cache.keys()) + ["properties", "id", "name", "refresh", "names", "props"]
41
+ keys = list(self._children_cache.keys())
37
42
  if self._level == "item":
38
43
  # Include property names directly on item for suggestions
39
44
  prop_keys = list(self._props_key_map().keys())
40
45
  keys.extend(prop_keys)
41
- return sorted(keys)
46
+ return sorted(set(keys))
42
47
 
43
- @property
44
- def id(self) -> Optional[str]:
45
- return self._id
46
-
47
- @property
48
- def name(self) -> Optional[str]:
49
- return self._name
50
-
51
- def refresh(self) -> "_Node":
48
+ # Intentionally no public id/name/refresh to keep suggestions minimal
49
+ def _refresh(self) -> "_Node":
52
50
  self._children_cache.clear()
53
51
  self._props_cache = None
54
52
  return self
55
53
 
56
- def names(self) -> List[str]:
57
- """Return display names of children at this level (forces a lazy load)."""
54
+ def _names(self) -> List[str]:
55
+ """Return display names of children at this level (internal)."""
58
56
  self._load_children()
59
57
  return [child._name or "" for child in self._children_cache.values()]
60
58
 
59
+ def _suggest(self) -> List[str]:
60
+ """Return suggested attribute names for interactive usage.
61
+
62
+ Only child keys are returned; for item level, property keys are also included.
63
+ """
64
+ self._load_children()
65
+ suggestions: List[str] = list(self._children_cache.keys())
66
+ if self._level == "item":
67
+ suggestions.extend(list(self._props_key_map().keys()))
68
+ return sorted(set(suggestions))
69
+
61
70
  def __getattr__(self, attr: str) -> Any:
62
- if attr in {"properties", "id", "name", "refresh"}:
63
- return object.__getattribute__(self, attr)
71
+ # No public properties/id/name/refresh
64
72
  if attr == "props": # item-level properties pseudo-node
65
73
  if self._level != "item":
66
74
  raise AttributeError("props")
@@ -92,8 +100,7 @@ class _Node:
92
100
  return self._children_cache[safe]
93
101
  raise KeyError(key)
94
102
 
95
- @property
96
- def properties(self) -> List[Dict[str, Any]]:
103
+ def _properties(self) -> List[Dict[str, Any]]:
97
104
  if self._props_cache is not None:
98
105
  return self._props_cache
99
106
  if self._level != "item":
@@ -138,7 +145,7 @@ class _Node:
138
145
  out: Dict[str, Dict[str, Any]] = {}
139
146
  if self._level != "item":
140
147
  return out
141
- props = self.properties
148
+ props = self._properties()
142
149
  used_names: Dict[str, int] = {}
143
150
  for i, pr in enumerate(props):
144
151
  # Try to get name from various possible fields
@@ -188,7 +195,7 @@ class _Node:
188
195
  return
189
196
  q = (
190
197
  "query($pid: ID!, $parent: ID!, $limit: Int!, $offset: Int!) {\n"
191
- " items(productId: $pid, parentItemId: $parent, limit: $limit, offset: $offset) { id name code description productId parentId owner }\n"
198
+ " items(productId: $pid, parentItemId: $parent, limit: $limit, offset: $offset) { id name code description productId parentId owner position }\n"
192
199
  "}"
193
200
  )
194
201
  r = self._client._transport.graphql(q, {"pid": pid, "parent": self._id, "limit": 1000, "offset": 0})
@@ -211,6 +218,15 @@ class Browser:
211
218
 
212
219
  def __init__(self, client: Any) -> None:
213
220
  self._root = _Node(client, "root", None, None, None)
221
+ # Best-effort: auto-enable curated completion in interactive shells
222
+ global _AUTO_COMPLETER_INSTALLED
223
+ if not _AUTO_COMPLETER_INSTALLED:
224
+ try:
225
+ if enable_dynamic_completion():
226
+ _AUTO_COMPLETER_INSTALLED = True
227
+ except Exception:
228
+ # Non-interactive or IPython not available; ignore silently
229
+ pass
214
230
 
215
231
  def __getattr__(self, attr: str) -> Any: # pragma: no cover - notebook UX
216
232
  return getattr(self._root, attr)
@@ -227,7 +243,15 @@ class Browser:
227
243
  def __dir__(self) -> list[str]: # pragma: no cover - notebook UX
228
244
  # Ensure children are loaded so TAB shows options
229
245
  self._root._load_children()
230
- return sorted([*self._root._children_cache.keys(), "names"])
246
+ return sorted([*self._root._children_cache.keys()])
247
+
248
+ def _names(self) -> List[str]:
249
+ """Return display names of root-level children (workspaces)."""
250
+ return self._root._names()
251
+
252
+ # keep suggest internal so it doesn't appear in help/dir
253
+ def _suggest(self) -> List[str]:
254
+ return self._root._suggest()
231
255
 
232
256
 
233
257
  def _safe_key(name: str) -> str:
@@ -255,7 +279,7 @@ class _PropsNode:
255
279
  def _ensure_loaded(self) -> None:
256
280
  if self._children_cache:
257
281
  return
258
- props = self._item.properties
282
+ props = self._item._properties()
259
283
  used_names: Dict[str, int] = {}
260
284
  names_list = []
261
285
  for i, pr in enumerate(props):
@@ -276,7 +300,7 @@ class _PropsNode:
276
300
 
277
301
  def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
278
302
  self._ensure_loaded()
279
- return sorted(list(self._children_cache.keys()) + ["names"])
303
+ return sorted(list(self._children_cache.keys()))
280
304
 
281
305
  def names(self) -> List[str]:
282
306
  self._ensure_loaded()
@@ -294,13 +318,21 @@ class _PropsNode:
294
318
  return self._children_cache[key]
295
319
  # match by display name
296
320
  for safe, data in self._children_cache.items():
297
- if data.raw.get("name") == key:
298
- return data
321
+ try:
322
+ if getattr(data, "_raw", {}).get("name") == key: # type: ignore[arg-type]
323
+ return data
324
+ except Exception:
325
+ continue
299
326
  safe = _safe_key(key)
300
327
  if safe in self._children_cache:
301
328
  return self._children_cache[safe]
302
329
  raise KeyError(key)
303
330
 
331
+ # keep suggest internal so it doesn't appear in help/dir
332
+ def _suggest(self) -> List[str]:
333
+ self._ensure_loaded()
334
+ return sorted(list(self._children_cache.keys()))
335
+
304
336
 
305
337
  class _PropWrapper:
306
338
  """Lightweight accessor for a property dict, exposing `.value` and `.raw`.
@@ -311,10 +343,6 @@ class _PropWrapper:
311
343
  def __init__(self, prop: Dict[str, Any]) -> None:
312
344
  self._raw = prop
313
345
 
314
- @property
315
- def raw(self) -> Dict[str, Any]:
316
- return self._raw
317
-
318
346
  @property
319
347
  def value(self) -> Any: # type: ignore[override]
320
348
  p = self._raw
@@ -337,8 +365,123 @@ class _PropWrapper:
337
365
  return p.get("value")
338
366
  return None
339
367
 
368
+ @property
369
+ def category(self) -> Optional[str]:
370
+ p = self._raw
371
+ cat = p.get("category")
372
+ return str(cat) if cat is not None else None
373
+
374
+ def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
375
+ # Expose only the minimal attributes for browsing
376
+ return ["value", "category"]
377
+
340
378
  def __repr__(self) -> str: # pragma: no cover - notebook UX
341
379
  name = self._raw.get("name") or self._raw.get("id")
342
380
  return f"<property {name}: {self.value}>"
343
381
 
344
382
 
383
+
384
+ def enable_dynamic_completion() -> bool:
385
+ """Enable dynamic attribute completion in IPython/Jupyter environments.
386
+
387
+ This helper attempts to configure IPython to use runtime-based completion
388
+ (disabling Jedi) so that our dynamic `__dir__` and `suggest()` methods are
389
+ respected by TAB completion. Returns True if an interactive shell was found
390
+ and configured, False otherwise.
391
+ """
392
+
393
+ try:
394
+ # Deferred import to avoid hard dependency
395
+ from IPython import get_ipython # type: ignore
396
+ except Exception:
397
+ return False
398
+
399
+ ip = None
400
+ try:
401
+ ip = get_ipython() # type: ignore[assignment]
402
+ except Exception:
403
+ ip = None
404
+ if ip is None:
405
+ return False
406
+
407
+ enabled = False
408
+ # Best-effort configuration: rely on IPython's fallback (non-Jedi) completer
409
+ try:
410
+ if hasattr(ip, "Completer") and hasattr(ip.Completer, "use_jedi"):
411
+ # Disable Jedi to let IPython consult __dir__ dynamically
412
+ ip.Completer.use_jedi = False # type: ignore[assignment]
413
+ # Greedy completion improves attribute completion depth
414
+ if hasattr(ip.Completer, "greedy"):
415
+ ip.Completer.greedy = True # type: ignore[assignment]
416
+ enabled = True
417
+ except Exception:
418
+ pass
419
+
420
+ # Additionally, install a lightweight attribute completer that uses suggest()
421
+ try:
422
+ comp = getattr(ip, "Completer", None)
423
+ if comp is not None and hasattr(comp, "attr_matches"):
424
+ orig_attr_matches = comp.attr_matches # type: ignore[attr-defined]
425
+
426
+ def _poelis_attr_matches(self: Any, text: str) -> List[str]: # pragma: no cover - interactive behavior
427
+ try:
428
+ # text is like "client.browser.uh2.pr" → split at last dot
429
+ obj_expr, _, prefix = text.rpartition(".")
430
+ if not obj_expr:
431
+ return orig_attr_matches(text) # type: ignore[operator]
432
+ # Evaluate the object in the user namespace
433
+ ns = getattr(self, "namespace", {})
434
+ obj_val = eval(obj_expr, ns, ns)
435
+
436
+ # For Poelis browser objects, show ONLY our curated suggestions
437
+ from_types = (Browser, _Node, _PropsNode, _PropWrapper)
438
+ if isinstance(obj_val, from_types):
439
+ # Build suggestion list
440
+ if isinstance(obj_val, _PropWrapper):
441
+ sugg: List[str] = ["value", "category"]
442
+ elif hasattr(obj_val, "_suggest"):
443
+ sugg = list(getattr(obj_val, "_suggest")()) # type: ignore[no-untyped-call]
444
+ else:
445
+ sugg = list(dir(obj_val))
446
+ # Filter by prefix and format matches as full attribute paths
447
+ out: List[str] = []
448
+ for s in sugg:
449
+ if not prefix or str(s).startswith(prefix):
450
+ out.append(f"{obj_expr}.{s}")
451
+ return out
452
+
453
+ # Otherwise, fall back to default behavior
454
+ return orig_attr_matches(text) # type: ignore[operator]
455
+ except Exception:
456
+ # fall back to original on any error
457
+ return orig_attr_matches(text) # type: ignore[operator]
458
+
459
+ comp.attr_matches = MethodType(_poelis_attr_matches, comp) # type: ignore[assignment]
460
+ enabled = True
461
+ except Exception:
462
+ pass
463
+
464
+ # Also register as a high-priority matcher in IPCompleter.matchers
465
+ try:
466
+ comp = getattr(ip, "Completer", None)
467
+ if comp is not None and hasattr(comp, "matchers") and not getattr(comp, "_poelis_matcher_installed", False):
468
+ orig_attr_matches = comp.attr_matches # type: ignore[attr-defined]
469
+
470
+ def _poelis_matcher(self: Any, text: str) -> List[str]: # pragma: no cover - interactive behavior
471
+ # Delegate to our attribute logic for dotted expressions; otherwise empty
472
+ if "." in text:
473
+ try:
474
+ return self.attr_matches(text) # type: ignore[operator]
475
+ except Exception:
476
+ return orig_attr_matches(text) # type: ignore[operator]
477
+ return []
478
+
479
+ # Prepend our matcher so it's consulted early
480
+ comp.matchers.insert(0, MethodType(_poelis_matcher, comp)) # type: ignore[arg-type]
481
+ setattr(comp, "_poelis_matcher_installed", True)
482
+ enabled = True
483
+ except Exception:
484
+ pass
485
+
486
+ return bool(enabled)
487
+
@@ -21,7 +21,7 @@ class ItemsClient:
21
21
 
22
22
  query = (
23
23
  "query($pid: ID!, $q: String, $limit: Int!, $offset: Int!) {\n"
24
- " items(productId: $pid, q: $q, limit: $limit, offset: $offset) { id name code description productId parentId owner }\n"
24
+ " items(productId: $pid, q: $q, limit: $limit, offset: $offset) { id name code description productId parentId owner position }\n"
25
25
  "}"
26
26
  )
27
27
  variables = {"pid": product_id, "q": q, "limit": int(limit), "offset": int(offset)}
@@ -43,7 +43,7 @@ class ItemsClient:
43
43
 
44
44
  query = (
45
45
  "query($id: ID!) {\n"
46
- " item(id: $id) { id name code description productId parentId owner }\n"
46
+ " item(id: $id) { id name code description productId parentId owner position }\n"
47
47
  "}"
48
48
  )
49
49
  resp = self._t.graphql(query=query, variables={"id": item_id})
@@ -14,11 +14,11 @@ class SearchClient:
14
14
  self._t = transport
15
15
 
16
16
  def products(self, *, q: str, workspace_id: str, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
17
- """Search/list products via GraphQL products(workspace_id, q)."""
17
+ """Search/list products via GraphQL products(workspaceId, q)."""
18
18
 
19
19
  query = (
20
20
  "query($ws: ID!, $q: String, $limit: Int!, $offset: Int!) {\n"
21
- " products(workspace_id: $ws, q: $q, limit: $limit, offset: $offset) { id name code description workspace_id }\n"
21
+ " products(workspaceId: $ws, q: $q, limit: $limit, offset: $offset) { id name code description workspaceId }\n"
22
22
  "}"
23
23
  )
24
24
  variables = {"ws": workspace_id, "q": q, "limit": int(limit), "offset": int(offset)}
@@ -35,7 +35,7 @@ class SearchClient:
35
35
 
36
36
  query = (
37
37
  "query($pid: ID!, $q: String, $parent: ID, $limit: Int!, $offset: Int!) {\n"
38
- " items(productId: $pid, q: $q, parentItemId: $parent, limit: $limit, offset: $offset) { id name code description productId parentId owner }\n"
38
+ " items(productId: $pid, q: $q, parentItemId: $parent, limit: $limit, offset: $offset) { id name code description productId parentId owner position }\n"
39
39
  "}"
40
40
  )
41
41
  variables = {"pid": product_id, "q": q, "parent": parent_item_id, "limit": int(limit), "offset": int(offset)}