poelis-sdk 0.1.8__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.
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/PKG-INFO +1 -18
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/README.md +0 -17
- poelis_sdk-0.2.0/notebooks/try_poelis_sdk.ipynb +220 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/pyproject.toml +1 -1
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/browser.py +202 -37
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/items.py +3 -12
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/search.py +3 -3
- poelis_sdk-0.2.0/tests/test_browser_navigation.py +130 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/uv.lock +1 -1
- poelis_sdk-0.1.8/notebooks/try_poelis_sdk.ipynb +0 -635
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/.github/workflows/ci.yml +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/.github/workflows/codeql.yml +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/.github/workflows/publish-on-push.yml +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/.gitignore +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/LICENSE +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/__init__.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/_transport.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/auth0.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/client.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/exceptions.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/logging.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/models.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/org_validation.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/products.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/poelis_sdk/workspaces.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/tests/test_client_basic.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/tests/test_errors_and_backoff.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/tests/test_items_client.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/tests/test_search_client.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/src/tests/test_transport_and_products.py +0 -0
- {poelis_sdk-0.1.8 → poelis_sdk-0.2.0}/tests/__init__.py +0 -0
- {poelis_sdk-0.1.8 → 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.
|
|
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
|
+
}
|
|
@@ -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())
|
|
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
|
-
|
|
44
|
-
def
|
|
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
|
|
57
|
-
"""Return display names of children at this level (
|
|
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
|
-
|
|
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,18 +100,17 @@ class _Node:
|
|
|
92
100
|
return self._children_cache[safe]
|
|
93
101
|
raise KeyError(key)
|
|
94
102
|
|
|
95
|
-
|
|
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":
|
|
100
107
|
self._props_cache = []
|
|
101
108
|
return self._props_cache
|
|
102
|
-
# Try direct properties(
|
|
109
|
+
# Try direct properties(itemId: ...) first; fallback to searchProperties
|
|
103
110
|
q = (
|
|
104
111
|
"query($iid: ID!) {\n"
|
|
105
|
-
" properties(
|
|
106
|
-
" __typename
|
|
112
|
+
" properties(itemId: $iid) {\n"
|
|
113
|
+
" __typename\n"
|
|
107
114
|
" ... on NumericProperty { integerPart exponent category }\n"
|
|
108
115
|
" ... on TextProperty { value }\n"
|
|
109
116
|
" ... on DateProperty { value }\n"
|
|
@@ -121,7 +128,7 @@ class _Node:
|
|
|
121
128
|
q2 = (
|
|
122
129
|
"query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
|
|
123
130
|
" searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
|
|
124
|
-
" hits { id
|
|
131
|
+
" hits { id workspaceId productId itemId propertyType name category value owner }\n"
|
|
125
132
|
" }\n"
|
|
126
133
|
"}"
|
|
127
134
|
)
|
|
@@ -138,10 +145,20 @@ class _Node:
|
|
|
138
145
|
out: Dict[str, Dict[str, Any]] = {}
|
|
139
146
|
if self._level != "item":
|
|
140
147
|
return out
|
|
141
|
-
props = self.
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
props = self._properties()
|
|
149
|
+
used_names: Dict[str, int] = {}
|
|
150
|
+
for i, pr in enumerate(props):
|
|
151
|
+
# Try to get name from various possible fields
|
|
152
|
+
display = pr.get("name") or pr.get("id") or pr.get("category") or f"property_{i}"
|
|
144
153
|
safe = _safe_key(str(display))
|
|
154
|
+
|
|
155
|
+
# Handle duplicate names by adding a suffix
|
|
156
|
+
if safe in used_names:
|
|
157
|
+
used_names[safe] += 1
|
|
158
|
+
safe = f"{safe}_{used_names[safe]}"
|
|
159
|
+
else:
|
|
160
|
+
used_names[safe] = 0
|
|
161
|
+
|
|
145
162
|
out[safe] = _PropWrapper(pr)
|
|
146
163
|
return out
|
|
147
164
|
|
|
@@ -178,7 +195,7 @@ class _Node:
|
|
|
178
195
|
return
|
|
179
196
|
q = (
|
|
180
197
|
"query($pid: ID!, $parent: ID!, $limit: Int!, $offset: Int!) {\n"
|
|
181
|
-
" 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"
|
|
182
199
|
"}"
|
|
183
200
|
)
|
|
184
201
|
r = self._client._transport.graphql(q, {"pid": pid, "parent": self._id, "limit": 1000, "offset": 0})
|
|
@@ -201,6 +218,15 @@ class Browser:
|
|
|
201
218
|
|
|
202
219
|
def __init__(self, client: Any) -> None:
|
|
203
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
|
|
204
230
|
|
|
205
231
|
def __getattr__(self, attr: str) -> Any: # pragma: no cover - notebook UX
|
|
206
232
|
return getattr(self._root, attr)
|
|
@@ -217,7 +243,15 @@ class Browser:
|
|
|
217
243
|
def __dir__(self) -> list[str]: # pragma: no cover - notebook UX
|
|
218
244
|
# Ensure children are loaded so TAB shows options
|
|
219
245
|
self._root._load_children()
|
|
220
|
-
return sorted([*self._root._children_cache.keys()
|
|
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()
|
|
221
255
|
|
|
222
256
|
|
|
223
257
|
def _safe_key(name: str) -> str:
|
|
@@ -245,16 +279,28 @@ class _PropsNode:
|
|
|
245
279
|
def _ensure_loaded(self) -> None:
|
|
246
280
|
if self._children_cache:
|
|
247
281
|
return
|
|
248
|
-
props = self._item.
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
props = self._item._properties()
|
|
283
|
+
used_names: Dict[str, int] = {}
|
|
284
|
+
names_list = []
|
|
285
|
+
for i, pr in enumerate(props):
|
|
286
|
+
# Try to get name from various possible fields
|
|
287
|
+
display = pr.get("name") or pr.get("id") or pr.get("category") or f"property_{i}"
|
|
251
288
|
safe = _safe_key(str(display))
|
|
289
|
+
|
|
290
|
+
# Handle duplicate names by adding a suffix
|
|
291
|
+
if safe in used_names:
|
|
292
|
+
used_names[safe] += 1
|
|
293
|
+
safe = f"{safe}_{used_names[safe]}"
|
|
294
|
+
else:
|
|
295
|
+
used_names[safe] = 0
|
|
296
|
+
|
|
252
297
|
self._children_cache[safe] = _PropWrapper(pr)
|
|
253
|
-
|
|
298
|
+
names_list.append(display)
|
|
299
|
+
self._names = names_list
|
|
254
300
|
|
|
255
301
|
def __dir__(self) -> List[str]: # pragma: no cover - notebook UX
|
|
256
302
|
self._ensure_loaded()
|
|
257
|
-
return sorted(list(self._children_cache.keys())
|
|
303
|
+
return sorted(list(self._children_cache.keys()))
|
|
258
304
|
|
|
259
305
|
def names(self) -> List[str]:
|
|
260
306
|
self._ensure_loaded()
|
|
@@ -272,13 +318,21 @@ class _PropsNode:
|
|
|
272
318
|
return self._children_cache[key]
|
|
273
319
|
# match by display name
|
|
274
320
|
for safe, data in self._children_cache.items():
|
|
275
|
-
|
|
276
|
-
|
|
321
|
+
try:
|
|
322
|
+
if getattr(data, "_raw", {}).get("name") == key: # type: ignore[arg-type]
|
|
323
|
+
return data
|
|
324
|
+
except Exception:
|
|
325
|
+
continue
|
|
277
326
|
safe = _safe_key(key)
|
|
278
327
|
if safe in self._children_cache:
|
|
279
328
|
return self._children_cache[safe]
|
|
280
329
|
raise KeyError(key)
|
|
281
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
|
+
|
|
282
336
|
|
|
283
337
|
class _PropWrapper:
|
|
284
338
|
"""Lightweight accessor for a property dict, exposing `.value` and `.raw`.
|
|
@@ -289,10 +343,6 @@ class _PropWrapper:
|
|
|
289
343
|
def __init__(self, prop: Dict[str, Any]) -> None:
|
|
290
344
|
self._raw = prop
|
|
291
345
|
|
|
292
|
-
@property
|
|
293
|
-
def raw(self) -> Dict[str, Any]:
|
|
294
|
-
return self._raw
|
|
295
|
-
|
|
296
346
|
@property
|
|
297
347
|
def value(self) -> Any: # type: ignore[override]
|
|
298
348
|
p = self._raw
|
|
@@ -315,8 +365,123 @@ class _PropWrapper:
|
|
|
315
365
|
return p.get("value")
|
|
316
366
|
return None
|
|
317
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
|
+
|
|
318
378
|
def __repr__(self) -> str: # pragma: no cover - notebook UX
|
|
319
379
|
name = self._raw.get("name") or self._raw.get("id")
|
|
320
380
|
return f"<property {name}: {self.value}>"
|
|
321
381
|
|
|
322
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
|
+
|