whodb-sdk 0.127.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.
- whodb_sdk-0.127.0/PKG-INFO +83 -0
- whodb_sdk-0.127.0/README.md +63 -0
- whodb_sdk-0.127.0/pyproject.toml +30 -0
- whodb_sdk-0.127.0/setup.cfg +4 -0
- whodb_sdk-0.127.0/src/whodb/__init__.py +50 -0
- whodb_sdk-0.127.0/src/whodb/_async_ontology.py +173 -0
- whodb_sdk-0.127.0/src/whodb/_auth.py +122 -0
- whodb_sdk-0.127.0/src/whodb/_errors.py +57 -0
- whodb_sdk-0.127.0/src/whodb/_generated/__init__.py +2 -0
- whodb_sdk-0.127.0/src/whodb/_generated/hydration.py +34 -0
- whodb_sdk-0.127.0/src/whodb/_generated/manifest.py +81 -0
- whodb_sdk-0.127.0/src/whodb/_generated/operations.py +303 -0
- whodb_sdk-0.127.0/src/whodb/_generated/types.py +565 -0
- whodb_sdk-0.127.0/src/whodb/_hydrate.py +96 -0
- whodb_sdk-0.127.0/src/whodb/_manifest_check.py +59 -0
- whodb_sdk-0.127.0/src/whodb/_pagination.py +41 -0
- whodb_sdk-0.127.0/src/whodb/_transport.py +114 -0
- whodb_sdk-0.127.0/src/whodb/_transport_ipc.py +222 -0
- whodb_sdk-0.127.0/src/whodb/_version.py +4 -0
- whodb_sdk-0.127.0/src/whodb/client.py +289 -0
- whodb_sdk-0.127.0/src/whodb/dataset.py +52 -0
- whodb_sdk-0.127.0/src/whodb/ontology.py +349 -0
- whodb_sdk-0.127.0/src/whodb/source.py +75 -0
- whodb_sdk-0.127.0/src/whodb_sdk.egg-info/PKG-INFO +83 -0
- whodb_sdk-0.127.0/src/whodb_sdk.egg-info/SOURCES.txt +29 -0
- whodb_sdk-0.127.0/src/whodb_sdk.egg-info/dependency_links.txt +1 -0
- whodb_sdk-0.127.0/src/whodb_sdk.egg-info/requires.txt +1 -0
- whodb_sdk-0.127.0/src/whodb_sdk.egg-info/top_level.txt +1 -0
- whodb_sdk-0.127.0/tests/test_auth.py +88 -0
- whodb_sdk-0.127.0/tests/test_errors.py +54 -0
- whodb_sdk-0.127.0/tests/test_hydrate.py +47 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whodb-sdk
|
|
3
|
+
Version: 0.127.0
|
|
4
|
+
Summary: Official Python SDK for the WhoDB hosted platform — ontology, datasets, and sources as in-code function APIs
|
|
5
|
+
Author: Clidey, Inc.
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://whodb.com
|
|
8
|
+
Project-URL: Repository, https://github.com/clidey/whodb
|
|
9
|
+
Keywords: whodb,ontology,data,sdk
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
|
|
21
|
+
# whodb-sdk
|
|
22
|
+
|
|
23
|
+
Official Python SDK for the [WhoDB](https://whodb.com) hosted platform — your
|
|
24
|
+
ontology, datasets, and sources as in-code function APIs.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install whodb-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python ≥ 3.10.
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import os
|
|
38
|
+
from whodb import WhoDB
|
|
39
|
+
|
|
40
|
+
# Production: API key (create one in org settings → API keys).
|
|
41
|
+
whodb = WhoDB(api_key=os.environ["WHODB_API_KEY"])
|
|
42
|
+
|
|
43
|
+
# Local development: zero config — reuses your `whodb login` session.
|
|
44
|
+
# whodb = WhoDB()
|
|
45
|
+
|
|
46
|
+
users = whodb.ontology("User")
|
|
47
|
+
|
|
48
|
+
user = users.get("u_123")
|
|
49
|
+
active = users.list(where={"status": {"eq": "active"}}, page_size=100).all()
|
|
50
|
+
users.create({"email": "a@b.co"})
|
|
51
|
+
users.create_many(rows, idempotency_key="import-42")
|
|
52
|
+
users.update("u_123", {"plan": "pro"})
|
|
53
|
+
orders = users.follow_link("u_123", "orders").all()
|
|
54
|
+
|
|
55
|
+
# Iterate everything, page by page:
|
|
56
|
+
for page in users.list(page_size=500).pages():
|
|
57
|
+
print(len(page.rows))
|
|
58
|
+
|
|
59
|
+
# Async client:
|
|
60
|
+
from whodb import AsyncWhoDB
|
|
61
|
+
awhodb = AsyncWhoDB(api_key=os.environ["WHODB_API_KEY"])
|
|
62
|
+
user = await awhodb.ontology("User").get("u_123")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
Credential precedence: constructor args (`api_key=` / `token=` /
|
|
68
|
+
`credentials=`) → `WHODB_API_KEY` env var → the `whodb` CLI's stored login.
|
|
69
|
+
Workspace (`org=` / `project=`) is optional with an API key; pass `project=`
|
|
70
|
+
when the key has access to more than one project.
|
|
71
|
+
|
|
72
|
+
Inside a WhoDB Function, `WhoDB()` auto-detects the runtime and needs no
|
|
73
|
+
configuration at all.
|
|
74
|
+
|
|
75
|
+
## Typed clients
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
whodb sdk generate --language python --out whodb_gen/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
Apache-2.0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# whodb-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [WhoDB](https://whodb.com) hosted platform — your
|
|
4
|
+
ontology, datasets, and sources as in-code function APIs.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install whodb-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python ≥ 3.10.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import os
|
|
18
|
+
from whodb import WhoDB
|
|
19
|
+
|
|
20
|
+
# Production: API key (create one in org settings → API keys).
|
|
21
|
+
whodb = WhoDB(api_key=os.environ["WHODB_API_KEY"])
|
|
22
|
+
|
|
23
|
+
# Local development: zero config — reuses your `whodb login` session.
|
|
24
|
+
# whodb = WhoDB()
|
|
25
|
+
|
|
26
|
+
users = whodb.ontology("User")
|
|
27
|
+
|
|
28
|
+
user = users.get("u_123")
|
|
29
|
+
active = users.list(where={"status": {"eq": "active"}}, page_size=100).all()
|
|
30
|
+
users.create({"email": "a@b.co"})
|
|
31
|
+
users.create_many(rows, idempotency_key="import-42")
|
|
32
|
+
users.update("u_123", {"plan": "pro"})
|
|
33
|
+
orders = users.follow_link("u_123", "orders").all()
|
|
34
|
+
|
|
35
|
+
# Iterate everything, page by page:
|
|
36
|
+
for page in users.list(page_size=500).pages():
|
|
37
|
+
print(len(page.rows))
|
|
38
|
+
|
|
39
|
+
# Async client:
|
|
40
|
+
from whodb import AsyncWhoDB
|
|
41
|
+
awhodb = AsyncWhoDB(api_key=os.environ["WHODB_API_KEY"])
|
|
42
|
+
user = await awhodb.ontology("User").get("u_123")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Credential precedence: constructor args (`api_key=` / `token=` /
|
|
48
|
+
`credentials=`) → `WHODB_API_KEY` env var → the `whodb` CLI's stored login.
|
|
49
|
+
Workspace (`org=` / `project=`) is optional with an API key; pass `project=`
|
|
50
|
+
when the key has access to more than one project.
|
|
51
|
+
|
|
52
|
+
Inside a WhoDB Function, `WhoDB()` auto-detects the runtime and needs no
|
|
53
|
+
configuration at all.
|
|
54
|
+
|
|
55
|
+
## Typed clients
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
whodb sdk generate --language python --out whodb_gen/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
Apache-2.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "whodb-sdk"
|
|
7
|
+
version = "0.127.0"
|
|
8
|
+
description = "Official Python SDK for the WhoDB hosted platform — ontology, datasets, and sources as in-code function APIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["httpx>=0.27"]
|
|
13
|
+
authors = [{ name = "Clidey, Inc." }]
|
|
14
|
+
keywords = ["whodb", "ontology", "data", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://whodb.com"
|
|
27
|
+
Repository = "https://github.com/clidey/whodb"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Official Python SDK for the WhoDB hosted platform.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from whodb import WhoDB
|
|
5
|
+
|
|
6
|
+
whodb = WhoDB(api_key=os.environ["WHODB_API_KEY"])
|
|
7
|
+
user = whodb.ontology("User").get("u_123")
|
|
8
|
+
```
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._auth import CliCredentials, StaticCredentials
|
|
12
|
+
from ._errors import (
|
|
13
|
+
AuthError,
|
|
14
|
+
CliCredentialsError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PlatformError,
|
|
17
|
+
TransportCapabilityError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
WhoDBError,
|
|
20
|
+
WhoDBVersionError,
|
|
21
|
+
)
|
|
22
|
+
from ._pagination import ListCall, Page
|
|
23
|
+
from ._version import SDK_VERSION
|
|
24
|
+
from .client import DEFAULT_HOST, AsyncWhoDB, WhoDB
|
|
25
|
+
from .dataset import DatasetHandle
|
|
26
|
+
from .ontology import OntologyHandle
|
|
27
|
+
from .source import SourceHandle
|
|
28
|
+
|
|
29
|
+
__version__ = SDK_VERSION
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"WhoDB",
|
|
33
|
+
"AsyncWhoDB",
|
|
34
|
+
"DEFAULT_HOST",
|
|
35
|
+
"OntologyHandle",
|
|
36
|
+
"DatasetHandle",
|
|
37
|
+
"SourceHandle",
|
|
38
|
+
"ListCall",
|
|
39
|
+
"Page",
|
|
40
|
+
"WhoDBError",
|
|
41
|
+
"AuthError",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
"ValidationError",
|
|
44
|
+
"WhoDBVersionError",
|
|
45
|
+
"CliCredentialsError",
|
|
46
|
+
"TransportCapabilityError",
|
|
47
|
+
"PlatformError",
|
|
48
|
+
"StaticCredentials",
|
|
49
|
+
"CliCredentials",
|
|
50
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Async twin of OntologyHandle. Same behavior, awaitable surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from ._errors import NotFoundError, ValidationError
|
|
9
|
+
from ._generated import operations as ops
|
|
10
|
+
from ._hydrate import hydrate_rows, property_types_of
|
|
11
|
+
from ._manifest_check import warn_if_flagged
|
|
12
|
+
from .ontology import _DEFAULT_PAGE_SIZE, _to_record_inputs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncOntologyHandle:
|
|
16
|
+
"""Awaitable handle for one ontology entity, addressed by apiName."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
execute: Callable[[ops.Request], Awaitable[Any]],
|
|
21
|
+
project_id: Callable[[], Awaitable[str]],
|
|
22
|
+
api_name: str,
|
|
23
|
+
):
|
|
24
|
+
self._execute = execute
|
|
25
|
+
self._project_id = project_id
|
|
26
|
+
self._api_name = api_name
|
|
27
|
+
self._entity_cache: Optional[dict] = None
|
|
28
|
+
|
|
29
|
+
async def entity_meta(self) -> dict:
|
|
30
|
+
"""Resolve and cache the entity metadata backing this handle."""
|
|
31
|
+
if self._entity_cache is not None:
|
|
32
|
+
return self._entity_cache
|
|
33
|
+
warn_if_flagged("OntologyEntities")
|
|
34
|
+
entities = await self._execute(
|
|
35
|
+
ops.ontology_entities_request({"projectId": await self._project_id()})
|
|
36
|
+
)
|
|
37
|
+
entity = next((e for e in entities or [] if e.get("apiName") == self._api_name), None)
|
|
38
|
+
if entity is None:
|
|
39
|
+
raise NotFoundError(f'ontology entity "{self._api_name}" not found in this project')
|
|
40
|
+
self._entity_cache = entity
|
|
41
|
+
return entity
|
|
42
|
+
|
|
43
|
+
async def get(self, pk: Any) -> Optional[dict]:
|
|
44
|
+
"""Fetch a single record by primary key, or None when absent."""
|
|
45
|
+
entity = await self.entity_meta()
|
|
46
|
+
primary_key = entity.get("primaryKey")
|
|
47
|
+
if not primary_key:
|
|
48
|
+
raise ValidationError(
|
|
49
|
+
f'entity "{self._api_name}" has no primary key — use list() with a where filter'
|
|
50
|
+
)
|
|
51
|
+
warn_if_flagged("OntologyQuery")
|
|
52
|
+
result = await self._execute(
|
|
53
|
+
ops.ontology_query_request(
|
|
54
|
+
{
|
|
55
|
+
"projectId": await self._project_id(),
|
|
56
|
+
"input": {
|
|
57
|
+
"entity": self._api_name,
|
|
58
|
+
"whereJson": json.dumps({primary_key: {"eq": str(pk)}}),
|
|
59
|
+
"pageSize": 1,
|
|
60
|
+
"offset": 0,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
rows, _ = hydrate_rows(result, property_types_of(entity))
|
|
66
|
+
return rows[0] if rows else None
|
|
67
|
+
|
|
68
|
+
async def list(
|
|
69
|
+
self,
|
|
70
|
+
where: Optional[dict] = None,
|
|
71
|
+
sort: Optional[list[dict]] = None,
|
|
72
|
+
page_size: int = _DEFAULT_PAGE_SIZE,
|
|
73
|
+
page_offset: int = 0,
|
|
74
|
+
) -> list[dict]:
|
|
75
|
+
"""List one page of records with optional filter/sort."""
|
|
76
|
+
entity = await self.entity_meta()
|
|
77
|
+
warn_if_flagged("OntologyQuery")
|
|
78
|
+
result = await self._execute(
|
|
79
|
+
ops.ontology_query_request(
|
|
80
|
+
{
|
|
81
|
+
"projectId": await self._project_id(),
|
|
82
|
+
"input": {
|
|
83
|
+
"entity": self._api_name,
|
|
84
|
+
"whereJson": json.dumps(where) if where else None,
|
|
85
|
+
"sort": sort,
|
|
86
|
+
"pageSize": page_size,
|
|
87
|
+
"offset": page_offset,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
rows, _ = hydrate_rows(result, property_types_of(entity))
|
|
93
|
+
return rows
|
|
94
|
+
|
|
95
|
+
async def pages(
|
|
96
|
+
self, where: Optional[dict] = None, page_size: int = _DEFAULT_PAGE_SIZE
|
|
97
|
+
) -> AsyncIterator[list[dict]]:
|
|
98
|
+
"""Iterate every page until a short page signals the end."""
|
|
99
|
+
offset = 0
|
|
100
|
+
while True:
|
|
101
|
+
rows = await self.list(where=where, page_size=page_size, page_offset=offset)
|
|
102
|
+
yield rows
|
|
103
|
+
if len(rows) < page_size:
|
|
104
|
+
return
|
|
105
|
+
offset += page_size
|
|
106
|
+
|
|
107
|
+
async def create(self, values: dict[str, Any]) -> None:
|
|
108
|
+
"""Insert one record."""
|
|
109
|
+
entity = await self.entity_meta()
|
|
110
|
+
warn_if_flagged("OntologyAddRow")
|
|
111
|
+
await self._execute(
|
|
112
|
+
ops.ontology_add_row_request(
|
|
113
|
+
{
|
|
114
|
+
"projectId": await self._project_id(),
|
|
115
|
+
"entityId": entity["id"],
|
|
116
|
+
"values": _to_record_inputs(values),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def create_many(self, rows: list[dict[str, Any]], idempotency_key: Optional[str] = None) -> dict:
|
|
122
|
+
"""Insert many records with optional idempotency key."""
|
|
123
|
+
entity = await self.entity_meta()
|
|
124
|
+
warn_if_flagged("OntologyAddRows")
|
|
125
|
+
return await self._execute(
|
|
126
|
+
ops.ontology_add_rows_request(
|
|
127
|
+
{
|
|
128
|
+
"projectId": await self._project_id(),
|
|
129
|
+
"entityId": entity["id"],
|
|
130
|
+
"rows": [{"values": _to_record_inputs(row)} for row in rows],
|
|
131
|
+
"idempotencyKey": idempotency_key,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def update(self, pk: Any, values: dict[str, Any]) -> None:
|
|
137
|
+
"""Update one record identified by primary key."""
|
|
138
|
+
entity = await self.entity_meta()
|
|
139
|
+
primary_key = entity.get("primaryKey")
|
|
140
|
+
if not primary_key:
|
|
141
|
+
raise ValidationError(
|
|
142
|
+
f'entity "{self._api_name}" has no primary key — updates are not supported'
|
|
143
|
+
)
|
|
144
|
+
warn_if_flagged("OntologyUpdateRow")
|
|
145
|
+
await self._execute(
|
|
146
|
+
ops.ontology_update_row_request(
|
|
147
|
+
{
|
|
148
|
+
"projectId": await self._project_id(),
|
|
149
|
+
"entityId": entity["id"],
|
|
150
|
+
"values": _to_record_inputs({**values, primary_key: str(pk)}),
|
|
151
|
+
"updatedColumns": list(values.keys()),
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def delete(self, pk: Any) -> None:
|
|
157
|
+
"""Delete one record identified by primary key."""
|
|
158
|
+
entity = await self.entity_meta()
|
|
159
|
+
primary_key = entity.get("primaryKey")
|
|
160
|
+
if not primary_key:
|
|
161
|
+
raise ValidationError(
|
|
162
|
+
f'entity "{self._api_name}" has no primary key — deletes are not supported'
|
|
163
|
+
)
|
|
164
|
+
warn_if_flagged("OntologyDeleteRow")
|
|
165
|
+
await self._execute(
|
|
166
|
+
ops.ontology_delete_row_request(
|
|
167
|
+
{
|
|
168
|
+
"projectId": await self._project_id(),
|
|
169
|
+
"entityId": entity["id"],
|
|
170
|
+
"values": _to_record_inputs({primary_key: str(pk)}),
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Credential providers, mirrored from the TypeScript SDK's auth.ts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from typing import Callable, Optional, Protocol
|
|
9
|
+
|
|
10
|
+
from ._errors import AuthError, CliCredentialsError
|
|
11
|
+
|
|
12
|
+
_CLI_REFRESH_SKEW = timedelta(seconds=60)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CredentialProvider(Protocol):
|
|
16
|
+
"""Yields a bearer credential; refresh() runs once after a 401."""
|
|
17
|
+
|
|
18
|
+
def token(self) -> str: ...
|
|
19
|
+
|
|
20
|
+
def refresh(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
def defaults(self) -> dict:
|
|
23
|
+
"""Workspace defaults carried by the credential source, if any."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StaticCredentials:
|
|
28
|
+
"""Static API-key or raw-token credentials (production/headless usage)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, value: str):
|
|
31
|
+
self._value = value
|
|
32
|
+
|
|
33
|
+
def token(self) -> str:
|
|
34
|
+
"""Return the configured credential."""
|
|
35
|
+
return self._value
|
|
36
|
+
|
|
37
|
+
def refresh(self) -> None:
|
|
38
|
+
"""Static credentials cannot refresh; no-op."""
|
|
39
|
+
|
|
40
|
+
def defaults(self) -> dict:
|
|
41
|
+
"""Static credentials carry no workspace defaults."""
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CallbackCredentials:
|
|
46
|
+
"""Caller-managed token callback."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, callback: Callable[[], str]):
|
|
49
|
+
self._callback = callback
|
|
50
|
+
|
|
51
|
+
def token(self) -> str:
|
|
52
|
+
"""Return a token from the caller's callback."""
|
|
53
|
+
return self._callback()
|
|
54
|
+
|
|
55
|
+
def refresh(self) -> None:
|
|
56
|
+
"""The callback is consulted every call; nothing to invalidate."""
|
|
57
|
+
|
|
58
|
+
def defaults(self) -> dict:
|
|
59
|
+
"""Callback credentials carry no workspace defaults."""
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CliCredentials:
|
|
64
|
+
"""CLI credentials: exec `whodb auth print-token` and cache until expiry.
|
|
65
|
+
|
|
66
|
+
The gcloud-ADC pattern for local development — requires the whodb CLI on
|
|
67
|
+
PATH and a prior `whodb login`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, command: str = "whodb"):
|
|
71
|
+
self._command = command
|
|
72
|
+
self._cached: Optional[dict] = None
|
|
73
|
+
|
|
74
|
+
def _exec(self) -> dict:
|
|
75
|
+
try:
|
|
76
|
+
completed = subprocess.run(
|
|
77
|
+
[self._command, "auth", "print-token", "--format", "json"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
timeout=15,
|
|
80
|
+
check=False,
|
|
81
|
+
)
|
|
82
|
+
except FileNotFoundError as exc:
|
|
83
|
+
raise CliCredentialsError(
|
|
84
|
+
"whodb CLI not found — install it or set WHODB_API_KEY"
|
|
85
|
+
) from exc
|
|
86
|
+
if completed.returncode != 0:
|
|
87
|
+
detail = completed.stderr.decode(errors="replace").strip()
|
|
88
|
+
raise CliCredentialsError(f"whodb auth print-token failed: {detail}")
|
|
89
|
+
try:
|
|
90
|
+
return json.loads(completed.stdout)
|
|
91
|
+
except json.JSONDecodeError as exc:
|
|
92
|
+
raise CliCredentialsError("whodb auth print-token returned invalid JSON") from exc
|
|
93
|
+
|
|
94
|
+
def _is_fresh(self, entry: dict) -> bool:
|
|
95
|
+
expires_at = entry.get("expires_at")
|
|
96
|
+
if not expires_at:
|
|
97
|
+
return False # no expiry info — re-exec every call
|
|
98
|
+
expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
|
99
|
+
return expiry - datetime.now(timezone.utc) > _CLI_REFRESH_SKEW
|
|
100
|
+
|
|
101
|
+
def token(self) -> str:
|
|
102
|
+
"""Return a fresh access token, re-execing the CLI near expiry."""
|
|
103
|
+
if self._cached is None or not self._is_fresh(self._cached):
|
|
104
|
+
self._cached = self._exec()
|
|
105
|
+
token = self._cached.get("access_token")
|
|
106
|
+
if not token:
|
|
107
|
+
raise AuthError("whodb CLI returned an empty access token")
|
|
108
|
+
return token
|
|
109
|
+
|
|
110
|
+
def refresh(self) -> None:
|
|
111
|
+
"""Drop the cached token so the next call re-execs the CLI."""
|
|
112
|
+
self._cached = None
|
|
113
|
+
|
|
114
|
+
def defaults(self) -> dict:
|
|
115
|
+
"""Return the CLI's saved host/org/project defaults."""
|
|
116
|
+
if self._cached is None:
|
|
117
|
+
self._cached = self._exec()
|
|
118
|
+
return {
|
|
119
|
+
"host": self._cached.get("host"),
|
|
120
|
+
"org_id": self._cached.get("org_id"),
|
|
121
|
+
"project_id": self._cached.get("project_id"),
|
|
122
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Error taxonomy for the whodb SDK, mirrored across all SDK languages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WhoDBError(Exception):
|
|
7
|
+
"""Base class for all errors raised by the whodb SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthError(WhoDBError):
|
|
11
|
+
"""Authentication failed: missing, invalid, expired, or revoked credentials."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NotFoundError(WhoDBError):
|
|
15
|
+
"""The requested resource does not exist or the caller cannot see it."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ValidationError(WhoDBError):
|
|
19
|
+
"""The request was rejected as invalid before execution."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WhoDBVersionError(WhoDBError):
|
|
23
|
+
"""This SDK release targets an older platform API; upgrade the package."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CliCredentialsError(WhoDBError):
|
|
27
|
+
"""The whodb CLI credential helper is unavailable or not logged in."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TransportCapabilityError(WhoDBError):
|
|
31
|
+
"""An operation is not available over the current transport (e.g. IPC)."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlatformError(WhoDBError):
|
|
35
|
+
"""Any other platform-reported error, carrying the GraphQL error code."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str, code: str):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.code = code
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def map_graphql_errors(errors: list[dict]) -> WhoDBError:
|
|
43
|
+
"""Map a GraphQL errors array to the SDK error taxonomy.
|
|
44
|
+
|
|
45
|
+
The first error decides the type; its code is preserved on PlatformError
|
|
46
|
+
for callers that need to branch on specifics.
|
|
47
|
+
"""
|
|
48
|
+
first = errors[0] if errors else {"message": "unknown platform error"}
|
|
49
|
+
code = (first.get("extensions") or {}).get("code", "")
|
|
50
|
+
message = first.get("message", "unknown platform error")
|
|
51
|
+
if code in ("UNAUTHENTICATED", "FORBIDDEN"):
|
|
52
|
+
return AuthError(message)
|
|
53
|
+
if code == "NOT_FOUND":
|
|
54
|
+
return NotFoundError(message)
|
|
55
|
+
if code in ("BAD_USER_INPUT", "GRAPHQL_VALIDATION_FAILED"):
|
|
56
|
+
return ValidationError(message)
|
|
57
|
+
return PlatformError(message, code)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# GENERATED by sdk/tools/generate-core.mjs — DO NOT EDIT (manifest 4b84678f5fe7)
|
|
2
|
+
"""Column-type → coercion kind rules shared across all SDK languages."""
|
|
3
|
+
|
|
4
|
+
HYDRATION_RULES = {
|
|
5
|
+
"int": "int",
|
|
6
|
+
"integer": "int",
|
|
7
|
+
"bigint": "int",
|
|
8
|
+
"smallint": "int",
|
|
9
|
+
"int2": "int",
|
|
10
|
+
"int4": "int",
|
|
11
|
+
"int8": "int",
|
|
12
|
+
"serial": "int",
|
|
13
|
+
"bigserial": "int",
|
|
14
|
+
"float": "float",
|
|
15
|
+
"float4": "float",
|
|
16
|
+
"float8": "float",
|
|
17
|
+
"double": "float",
|
|
18
|
+
"double precision": "float",
|
|
19
|
+
"real": "float",
|
|
20
|
+
"numeric": "float",
|
|
21
|
+
"decimal": "float",
|
|
22
|
+
"bool": "bool",
|
|
23
|
+
"boolean": "bool",
|
|
24
|
+
"timestamp": "timestamp",
|
|
25
|
+
"timestamptz": "timestamp",
|
|
26
|
+
"timestamp with time zone": "timestamp",
|
|
27
|
+
"timestamp without time zone": "timestamp",
|
|
28
|
+
"datetime": "timestamp",
|
|
29
|
+
"date": "date",
|
|
30
|
+
"json": "json",
|
|
31
|
+
"jsonb": "json"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
HYDRATION_DEFAULT = "string"
|