poelis-sdk 0.2.1__tar.gz → 0.3.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.2.1 → poelis_sdk-0.3.0}/PKG-INFO +20 -2
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/README.md +19 -1
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/notebooks/try_poelis_sdk.ipynb +53 -26
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/pyproject.toml +1 -1
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/_transport.py +21 -7
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/browser.py +16 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/client.py +3 -3
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/tests/test_client_basic.py +3 -2
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/tests/test_transport_and_products.py +4 -2
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/tests/test_browser_navigation.py +2 -10
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/uv.lock +1 -1
- poelis_sdk-0.2.1/src/poelis_sdk/auth0.py +0 -87
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/.github/workflows/ci.yml +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/.github/workflows/codeql.yml +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/.github/workflows/publish-on-push.yml +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/.gitignore +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/LICENSE +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/__init__.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/exceptions.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/items.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/logging.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/models.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/org_validation.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/products.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/search.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/poelis_sdk/workspaces.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/tests/test_errors_and_backoff.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/tests/test_items_client.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/src/tests/test_search_client.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.0}/tests/__init__.py +0 -0
- {poelis_sdk-0.2.1 → poelis_sdk-0.3.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.3.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
|
|
@@ -60,6 +60,24 @@ export POELIS_ORG_ID=tenant_id_001
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
### How authentication works
|
|
64
|
+
|
|
65
|
+
The SDK does not talk to Auth0. It sends your API key directly to the Poelis backend for validation on every request.
|
|
66
|
+
|
|
67
|
+
- Default headers sent by the SDK:
|
|
68
|
+
|
|
69
|
+
- `X-API-Key: <api_key>` (and `X-Poelis-Api-Key` as a compatibility alias)
|
|
70
|
+
- `Authorization: Api-Key <api_key>` (compatibility for gateways expecting Authorization-only)
|
|
71
|
+
- `X-Poelis-Org: <org_id>`
|
|
72
|
+
|
|
73
|
+
You can opt into Bearer mode (legacy) by setting `POELIS_AUTH_MODE=bearer`, which will send:
|
|
74
|
+
|
|
75
|
+
- `Authorization: Bearer <api_key>`
|
|
76
|
+
- `X-Poelis-Org: <org_id>`
|
|
77
|
+
|
|
78
|
+
The backend validates the API key against your organization, applies authorization and filtering, and returns data.
|
|
79
|
+
|
|
80
|
+
|
|
63
81
|
## Dot-path browser (Notebook UX)
|
|
64
82
|
|
|
65
83
|
The SDK exposes a dot-path browser for easy exploration:
|
|
@@ -69,7 +87,7 @@ client.browser # then use TAB to explore
|
|
|
69
87
|
# client.browser.<workspace>.<product>.<item>.<child>.properties
|
|
70
88
|
```
|
|
71
89
|
|
|
72
|
-
See the example notebook in `notebooks/try_poelis_sdk.ipynb` for an end-to-end walkthrough (authentication, listing workspaces/products/items, and simple search queries).
|
|
90
|
+
See the example notebook in `notebooks/try_poelis_sdk.ipynb` for an end-to-end walkthrough (authentication, listing workspaces/products/items, and simple search queries). The client defaults to `https://api.poelis.ai` unless `POELIS_BASE_URL` is set.
|
|
73
91
|
|
|
74
92
|
## Requirements
|
|
75
93
|
|
|
@@ -37,6 +37,24 @@ export POELIS_ORG_ID=tenant_id_001
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
### How authentication works
|
|
41
|
+
|
|
42
|
+
The SDK does not talk to Auth0. It sends your API key directly to the Poelis backend for validation on every request.
|
|
43
|
+
|
|
44
|
+
- Default headers sent by the SDK:
|
|
45
|
+
|
|
46
|
+
- `X-API-Key: <api_key>` (and `X-Poelis-Api-Key` as a compatibility alias)
|
|
47
|
+
- `Authorization: Api-Key <api_key>` (compatibility for gateways expecting Authorization-only)
|
|
48
|
+
- `X-Poelis-Org: <org_id>`
|
|
49
|
+
|
|
50
|
+
You can opt into Bearer mode (legacy) by setting `POELIS_AUTH_MODE=bearer`, which will send:
|
|
51
|
+
|
|
52
|
+
- `Authorization: Bearer <api_key>`
|
|
53
|
+
- `X-Poelis-Org: <org_id>`
|
|
54
|
+
|
|
55
|
+
The backend validates the API key against your organization, applies authorization and filtering, and returns data.
|
|
56
|
+
|
|
57
|
+
|
|
40
58
|
## Dot-path browser (Notebook UX)
|
|
41
59
|
|
|
42
60
|
The SDK exposes a dot-path browser for easy exploration:
|
|
@@ -46,7 +64,7 @@ client.browser # then use TAB to explore
|
|
|
46
64
|
# client.browser.<workspace>.<product>.<item>.<child>.properties
|
|
47
65
|
```
|
|
48
66
|
|
|
49
|
-
See the example notebook in `notebooks/try_poelis_sdk.ipynb` for an end-to-end walkthrough (authentication, listing workspaces/products/items, and simple search queries).
|
|
67
|
+
See the example notebook in `notebooks/try_poelis_sdk.ipynb` for an end-to-end walkthrough (authentication, listing workspaces/products/items, and simple search queries). The client defaults to `https://api.poelis.ai` unless `POELIS_BASE_URL` is set.
|
|
50
68
|
|
|
51
69
|
## Requirements
|
|
52
70
|
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"source": [
|
|
69
69
|
"## 2. Authentication Setup\n",
|
|
70
70
|
"\n",
|
|
71
|
-
"Before you can use the SDK,
|
|
71
|
+
"Before you can use the SDK, obtain your API credentials from the Poelis web interface.\n",
|
|
72
72
|
"\n",
|
|
73
73
|
"### Getting Your Credentials\n",
|
|
74
74
|
"\n",
|
|
@@ -97,23 +97,33 @@
|
|
|
97
97
|
"\n",
|
|
98
98
|
"There are two ways to initialize the Poelis client:\n",
|
|
99
99
|
"\n",
|
|
100
|
-
"### Method 1: Direct Initialization (Simplified)\n"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"\n",
|
|
100
|
+
"### Method 1: Direct Initialization (Simplified)\n"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"cell_type": "code",
|
|
105
|
+
"execution_count": null,
|
|
106
|
+
"metadata": {},
|
|
107
|
+
"outputs": [],
|
|
108
|
+
"source": [
|
|
109
|
+
"# Manual initialization (replace with your credentials)\n",
|
|
110
|
+
"poelis = PoelisClient(\n",
|
|
111
|
+
" api_key=\"your_api_key_here\",\n",
|
|
112
|
+
" org_id=\"your_org_id_here\"\n",
|
|
113
|
+
")"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"cell_type": "markdown",
|
|
118
|
+
"metadata": {},
|
|
119
|
+
"source": [
|
|
110
120
|
"### Method 2: From Environment Variables (Recommended)\n",
|
|
111
121
|
"\n",
|
|
112
122
|
"```python\n",
|
|
113
123
|
"client = PoelisClient.from_env()\n",
|
|
114
124
|
"```\n",
|
|
115
125
|
"\n",
|
|
116
|
-
"This method automatically reads the `POELIS_API_KEY` and `POELIS_ORG_ID` environment variables
|
|
126
|
+
"This method automatically reads the `POELIS_API_KEY` and `POELIS_ORG_ID` environment variables."
|
|
117
127
|
]
|
|
118
128
|
},
|
|
119
129
|
{
|
|
@@ -128,19 +138,14 @@
|
|
|
128
138
|
"\n",
|
|
129
139
|
"try:\n",
|
|
130
140
|
" # Try to initialize from environment variables first\n",
|
|
131
|
-
"
|
|
141
|
+
" poelis = PoelisClient.from_env()\n",
|
|
132
142
|
" print(\"✅ Client initialized from environment variables\")\n",
|
|
133
143
|
" print(f\"Base URL: {client.base_url}\")\n",
|
|
134
144
|
" print(f\"Organization ID: {client.org_id}\")\n",
|
|
135
145
|
"except ValueError as e:\n",
|
|
136
146
|
" print(f\"❌ Environment variables not set: {e}\")\n",
|
|
137
147
|
" 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"
|
|
148
|
+
" \n"
|
|
144
149
|
]
|
|
145
150
|
},
|
|
146
151
|
{
|
|
@@ -171,7 +176,7 @@
|
|
|
171
176
|
"from poelis_sdk import PoelisClient\n",
|
|
172
177
|
"\n",
|
|
173
178
|
"# 1) Create the client\n",
|
|
174
|
-
"
|
|
179
|
+
"poelis = PoelisClient(\n",
|
|
175
180
|
" api_key=\"YOUR_API_KEY\",\n",
|
|
176
181
|
" org_id=\"YOUR_ORG_ID\",\n",
|
|
177
182
|
")\n",
|
|
@@ -180,19 +185,41 @@
|
|
|
180
185
|
"# - Type: client.browser. and press TAB to see workspaces\n",
|
|
181
186
|
"# - Then: client.browser.<workspace>. and TAB to see products\n",
|
|
182
187
|
"# - Then: client.browser.<workspace>.<product>. and TAB to see items\n",
|
|
183
|
-
"# - On an item, TAB shows child items and property keys
|
|
184
|
-
|
|
188
|
+
"# - On an item, TAB shows child items and property keys"
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"cell_type": "code",
|
|
193
|
+
"execution_count": null,
|
|
194
|
+
"metadata": {},
|
|
195
|
+
"outputs": [],
|
|
196
|
+
"source": [
|
|
185
197
|
"# 3) Access a property’s value and category via dot paths\n",
|
|
186
198
|
"# Example (replace names with your actual names shown by TAB):\n",
|
|
187
|
-
"workspace =
|
|
199
|
+
"workspace = poelis.browser.UH2_Workspace"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"cell_type": "code",
|
|
204
|
+
"execution_count": null,
|
|
205
|
+
"metadata": {},
|
|
206
|
+
"outputs": [],
|
|
207
|
+
"source": [
|
|
188
208
|
"val = workspace.LH2_Tank.cold_box.clv.mass.value\n",
|
|
189
209
|
"cat = workspace.LH2_Tank.cold_box.clv.mass.category\n",
|
|
190
210
|
"\n",
|
|
191
211
|
"print(\"property value:\", val)\n",
|
|
192
|
-
"print(\"property category:\", cat)
|
|
193
|
-
|
|
212
|
+
"print(\"property category:\", cat)"
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"cell_type": "code",
|
|
217
|
+
"execution_count": null,
|
|
218
|
+
"metadata": {},
|
|
219
|
+
"outputs": [],
|
|
220
|
+
"source": [
|
|
194
221
|
"# 4) Go deeper into child items if needed (TAB will suggest only item/property names)\n",
|
|
195
|
-
"inner =
|
|
222
|
+
"inner = poelis.browser.UH2_Workspace.LH2_Tank.inner_tank"
|
|
196
223
|
]
|
|
197
224
|
}
|
|
198
225
|
],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, Mapping, Optional
|
|
4
|
+
import os
|
|
4
5
|
import time
|
|
5
6
|
import random
|
|
6
7
|
import httpx
|
|
7
8
|
|
|
8
9
|
from .exceptions import ClientError, HTTPError, NotFoundError, RateLimitError, ServerError, UnauthorizedError
|
|
9
|
-
from .auth0 import Auth0TokenManager
|
|
10
10
|
|
|
11
11
|
"""HTTP transport abstraction for the Poelis SDK.
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ class Transport:
|
|
|
29
29
|
|
|
30
30
|
Args:
|
|
31
31
|
base_url: Base API URL.
|
|
32
|
-
api_key: API key
|
|
32
|
+
api_key: API key provided by backend to authenticate requests.
|
|
33
33
|
org_id: Organization id for tenant scoping.
|
|
34
34
|
timeout_seconds: Request timeout in seconds.
|
|
35
35
|
"""
|
|
@@ -38,18 +38,32 @@ class Transport:
|
|
|
38
38
|
self._api_key = api_key
|
|
39
39
|
self._org_id = org_id
|
|
40
40
|
self._timeout = timeout_seconds
|
|
41
|
+
# Auth mode is intentionally read from environment to keep the public
|
|
42
|
+
# constructor signature stable for tests and backwards-compatibility.
|
|
43
|
+
# Supported values:
|
|
44
|
+
# - "bearer" (default): Authorization: Bearer <api_key>
|
|
45
|
+
# - "api_key": X-API-Key/X-Poelis-Api-Key headers with no Authorization
|
|
46
|
+
self._auth_mode = os.environ.get("POELIS_AUTH_MODE", "api_key").strip().lower()
|
|
41
47
|
|
|
42
|
-
# Initialize Auth0 token manager
|
|
43
|
-
self._auth0_manager = Auth0TokenManager(api_key, org_id, base_url)
|
|
44
48
|
|
|
45
49
|
def _headers(self, extra: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
|
46
50
|
headers: Dict[str, str] = {
|
|
47
51
|
"Accept": "application/json",
|
|
48
52
|
"Content-Type": "application/json",
|
|
49
53
|
}
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
# Resolve auth mode with a safe fallback in case tests monkeypatch __init__
|
|
55
|
+
auth_mode = getattr(self, "_auth_mode", None) or os.environ.get("POELIS_AUTH_MODE", "api_key").strip().lower()
|
|
56
|
+
if auth_mode == "api_key":
|
|
57
|
+
# Some staging environments expect an API key header and reject Bearer
|
|
58
|
+
# Include common header variants for compatibility; backend may accept either.
|
|
59
|
+
headers["X-API-Key"] = self._api_key
|
|
60
|
+
headers["X-Poelis-Api-Key"] = self._api_key
|
|
61
|
+
# Additionally include Authorization with Api-Key scheme for services
|
|
62
|
+
# that only read credentials from Authorization.
|
|
63
|
+
headers["Authorization"] = f"Api-Key {self._api_key}"
|
|
64
|
+
else:
|
|
65
|
+
# Default: send API key as Bearer token
|
|
66
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
53
67
|
headers["X-Poelis-Org"] = self._org_id
|
|
54
68
|
if extra:
|
|
55
69
|
headers.update(dict(extra))
|
|
@@ -56,6 +56,10 @@ class _Node:
|
|
|
56
56
|
self._load_children()
|
|
57
57
|
return [child._name or "" for child in self._children_cache.values()]
|
|
58
58
|
|
|
59
|
+
def names(self) -> List[str]:
|
|
60
|
+
"""Public: return display names of children at this level."""
|
|
61
|
+
return self._names()
|
|
62
|
+
|
|
59
63
|
def _suggest(self) -> List[str]:
|
|
60
64
|
"""Return suggested attribute names for interactive usage.
|
|
61
65
|
|
|
@@ -249,10 +253,22 @@ class Browser:
|
|
|
249
253
|
"""Return display names of root-level children (workspaces)."""
|
|
250
254
|
return self._root._names()
|
|
251
255
|
|
|
256
|
+
def names(self) -> List[str]:
|
|
257
|
+
"""Public: return display names of root-level children (workspaces)."""
|
|
258
|
+
return self._root._names()
|
|
259
|
+
|
|
252
260
|
# keep suggest internal so it doesn't appear in help/dir
|
|
253
261
|
def _suggest(self) -> List[str]:
|
|
254
262
|
return self._root._suggest()
|
|
255
263
|
|
|
264
|
+
def suggest(self) -> List[str]:
|
|
265
|
+
"""Return curated attribute suggestions at the current root level.
|
|
266
|
+
|
|
267
|
+
This mirrors the internal `_suggest` used for interactive completion,
|
|
268
|
+
but is exposed publicly for tests and programmatic usage.
|
|
269
|
+
"""
|
|
270
|
+
return self._root._suggest()
|
|
271
|
+
|
|
256
272
|
|
|
257
273
|
def _safe_key(name: str) -> str:
|
|
258
274
|
"""Convert arbitrary display name to a safe attribute key (letters/digits/_)."""
|
|
@@ -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://poelis
|
|
34
|
+
base_url: HttpUrl = Field(default="https://api.poelis.ai")
|
|
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://poelis
|
|
48
|
+
def __init__(self, api_key: str, org_id: str, base_url: str = "https://api.poelis.ai", 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://poelis
|
|
93
|
+
base_url = os.environ.get("POELIS_BASE_URL", "https://api.poelis.ai")
|
|
94
94
|
api_key = os.environ.get("POELIS_API_KEY")
|
|
95
95
|
org_id = os.environ.get("POELIS_ORG_ID")
|
|
96
96
|
|
|
@@ -34,7 +34,7 @@ def test_client_default_base_url() -> None:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def test_client_api_key_headers(monkeypatch: "MonkeyPatch") -> None:
|
|
37
|
-
"""When api_key and org_id are provided, use
|
|
37
|
+
"""When api_key and org_id are provided, use API key and X-Poelis-Org headers by default."""
|
|
38
38
|
|
|
39
39
|
import httpx
|
|
40
40
|
from poelis_sdk.client import Transport as _T
|
|
@@ -62,7 +62,8 @@ def test_client_api_key_headers(monkeypatch: "MonkeyPatch") -> None:
|
|
|
62
62
|
# trigger a request to test headers
|
|
63
63
|
client._transport.get("/health")
|
|
64
64
|
assert t.last is not None
|
|
65
|
-
assert t.last.headers.get("
|
|
65
|
+
assert t.last.headers.get("X-API-Key") == "poelis_live_abc" or t.last.headers.get("X-Poelis-Api-Key") == "poelis_live_abc"
|
|
66
|
+
assert t.last.headers.get("Authorization") == "Api-Key poelis_live_abc"
|
|
66
67
|
assert t.last.headers.get("X-Poelis-Org") == "tenant_x"
|
|
67
68
|
finally:
|
|
68
69
|
_T.__init__ = orig # type: ignore[assignment]
|
|
@@ -46,7 +46,7 @@ class _MockTransport(httpx.BaseTransport):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_auth_header_and_pagination(monkeypatch: "MonkeyPatch") -> None:
|
|
49
|
-
"""Verify
|
|
49
|
+
"""Verify auth headers exist and pagination iterates over all pages."""
|
|
50
50
|
|
|
51
51
|
client = PoelisClient(base_url="http://example.com", api_key="k", org_id="o")
|
|
52
52
|
|
|
@@ -72,7 +72,9 @@ def test_auth_header_and_pagination(monkeypatch: "MonkeyPatch") -> None:
|
|
|
72
72
|
# Check headers on first request
|
|
73
73
|
assert mt.requests, "no requests captured"
|
|
74
74
|
first = mt.requests[0]
|
|
75
|
-
|
|
75
|
+
# Default auth mode is API key headers and Authorization: Api-Key
|
|
76
|
+
assert first.headers.get("X-API-Key") == "k" or first.headers.get("X-Poelis-Api-Key") == "k"
|
|
77
|
+
assert first.headers.get("Authorization") == "Api-Key k"
|
|
76
78
|
assert first.headers.get("Accept") == "application/json"
|
|
77
79
|
finally:
|
|
78
80
|
_T.__init__ = _orig_init # type: ignore[assignment]
|
|
@@ -6,6 +6,7 @@ These tests avoid reliance on IPython and focus on programmatic APIs.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any, Dict
|
|
9
|
+
import json
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ class _MockTransport(httpx.BaseTransport):
|
|
|
22
23
|
def handle_request(self, request: httpx.Request) -> httpx.Response: # type: ignore[override]
|
|
23
24
|
self.requests.append(request)
|
|
24
25
|
if request.method == "POST" and request.url.path == "/v1/graphql":
|
|
25
|
-
payload = request.
|
|
26
|
+
payload = json.loads(request.content.decode("utf-8"))
|
|
26
27
|
query: str = payload.get("query", "")
|
|
27
28
|
vars: Dict[str, Any] = payload.get("variables", {})
|
|
28
29
|
|
|
@@ -77,15 +78,6 @@ def _client_with_graphql_mock(t: httpx.BaseTransport) -> PoelisClient:
|
|
|
77
78
|
self._org_id = org_id
|
|
78
79
|
self._timeout = timeout_seconds
|
|
79
80
|
|
|
80
|
-
# minimal stubs for headers used by transport
|
|
81
|
-
class _Auth0:
|
|
82
|
-
def __init__(self) -> None:
|
|
83
|
-
pass
|
|
84
|
-
|
|
85
|
-
def get_token(self) -> str:
|
|
86
|
-
return "tok"
|
|
87
|
-
|
|
88
|
-
self._auth0_manager = _Auth0()
|
|
89
81
|
|
|
90
82
|
orig = _T.__init__
|
|
91
83
|
_T.__init__ = _init # type: ignore[assignment]
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Auth0 Client Credentials flow for SDK authentication.
|
|
2
|
-
|
|
3
|
-
This module handles automatic JWT token acquisition using Auth0 Client Credentials
|
|
4
|
-
flow when users provide API keys. The API key serves as the client_id, and we
|
|
5
|
-
derive the client_secret and audience from the API key format.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import time
|
|
11
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
import httpx
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Auth0TokenManager:
|
|
17
|
-
"""Manages Auth0 JWT tokens using Client Credentials flow.
|
|
18
|
-
|
|
19
|
-
Automatically acquires and refreshes tokens based on API keys.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(self, api_key: str, org_id: str, base_url: str) -> None:
|
|
23
|
-
"""Initialize token manager.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
api_key: API key from webapp (used as client_id)
|
|
27
|
-
org_id: Organization ID for audience
|
|
28
|
-
base_url: Base URL to derive Auth0 domain and audience
|
|
29
|
-
"""
|
|
30
|
-
self.api_key = api_key
|
|
31
|
-
self.org_id = org_id
|
|
32
|
-
self.base_url = base_url
|
|
33
|
-
|
|
34
|
-
# Extract Auth0 domain from API key format
|
|
35
|
-
# Format: poelis_live_org_dev_<client_id>
|
|
36
|
-
parts = api_key.split('_')
|
|
37
|
-
if len(parts) >= 4 and parts[0] == 'poelis' and parts[1] == 'live' and parts[2] == 'org' and parts[3] == 'dev':
|
|
38
|
-
# For now, use the Machine to Machine application credentials
|
|
39
|
-
# TODO: Parse client_id from API key when webapp generates proper format
|
|
40
|
-
self.client_id = "XcSLURURuQNEVvX2PF5DplNhTY6YCT4C" # Machine to Machine app
|
|
41
|
-
self.client_secret = "TM_Fv8FsfAaqvODf7ayyE_LrZM2KbbpdtLIIMqkIZwFXfKYLdOFcO2qmyO0v970-"
|
|
42
|
-
else:
|
|
43
|
-
raise ValueError("Invalid API key format. Expected: poelis_live_org_dev_<client_id>")
|
|
44
|
-
|
|
45
|
-
# Derive Auth0 domain and audience
|
|
46
|
-
self.auth0_domain = "poelis-prod.eu.auth0.com"
|
|
47
|
-
self.audience = "poelis-auth-api" # Use the API identifier, not the GCP URL
|
|
48
|
-
|
|
49
|
-
self._token: Optional[str] = None
|
|
50
|
-
self._expires_at: float = 0
|
|
51
|
-
|
|
52
|
-
def get_token(self) -> str:
|
|
53
|
-
"""Get valid JWT token, refreshing if needed.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
str: JWT token for Authorization header
|
|
57
|
-
"""
|
|
58
|
-
if self._token and time.time() < self._expires_at - 60: # Refresh 1min early
|
|
59
|
-
return self._token
|
|
60
|
-
|
|
61
|
-
return self._refresh_token()
|
|
62
|
-
|
|
63
|
-
def _refresh_token(self) -> str:
|
|
64
|
-
"""Acquire new JWT token from Auth0.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
str: Fresh JWT token
|
|
68
|
-
"""
|
|
69
|
-
token_url = f"https://{self.auth0_domain}/oauth/token"
|
|
70
|
-
|
|
71
|
-
data = {
|
|
72
|
-
"client_id": self.client_id,
|
|
73
|
-
"client_secret": self.client_secret,
|
|
74
|
-
"audience": self.audience,
|
|
75
|
-
"grant_type": "client_credentials"
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
with httpx.Client() as client:
|
|
79
|
-
response = client.post(token_url, data=data)
|
|
80
|
-
response.raise_for_status()
|
|
81
|
-
|
|
82
|
-
token_data = response.json()
|
|
83
|
-
self._token = token_data["access_token"]
|
|
84
|
-
expires_in = token_data.get("expires_in", 3600)
|
|
85
|
-
self._expires_at = time.time() + expires_in
|
|
86
|
-
|
|
87
|
-
return self._token
|
|
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
|