provably-sdk 0.1.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.
- provably_sdk-0.1.0/.github/workflows/publish.yml +34 -0
- provably_sdk-0.1.0/.gitignore +7 -0
- provably_sdk-0.1.0/LICENSE.md +101 -0
- provably_sdk-0.1.0/PKG-INFO +92 -0
- provably_sdk-0.1.0/README.md +62 -0
- provably_sdk-0.1.0/pyproject.toml +67 -0
- provably_sdk-0.1.0/src/provably/__init__.py +55 -0
- provably_sdk-0.1.0/src/provably/_answer_model.py +78 -0
- provably_sdk-0.1.0/src/provably/_api.py +407 -0
- provably_sdk-0.1.0/src/provably/_auth_api.py +191 -0
- provably_sdk-0.1.0/src/provably/_config.py +136 -0
- provably_sdk-0.1.0/src/provably/_errors.py +162 -0
- provably_sdk-0.1.0/src/provably/_http.py +247 -0
- provably_sdk-0.1.0/src/provably/auth_service.py +149 -0
- provably_sdk-0.1.0/src/provably/connection.py +43 -0
- provably_sdk-0.1.0/src/provably/oauth_login.py +133 -0
- provably_sdk-0.1.0/src/provably/py.typed +0 -0
- provably_sdk-0.1.0/src/provably/service.py +674 -0
- provably_sdk-0.1.0/tests/__init__.py +0 -0
- provably_sdk-0.1.0/tests/test_answer_model.py +113 -0
- provably_sdk-0.1.0/tests/test_api.py +67 -0
- provably_sdk-0.1.0/tests/test_auth_api.py +110 -0
- provably_sdk-0.1.0/tests/test_auth_service.py +170 -0
- provably_sdk-0.1.0/tests/test_connection.py +20 -0
- provably_sdk-0.1.0/tests/test_errors.py +211 -0
- provably_sdk-0.1.0/tests/test_http.py +267 -0
- provably_sdk-0.1.0/tests/test_oauth_login.py +93 -0
- provably_sdk-0.1.0/tests/test_sandbox_api.py +105 -0
- provably_sdk-0.1.0/tests/test_sandbox_service.py +187 -0
- provably_sdk-0.1.0/tests/test_service.py +164 -0
- provably_sdk-0.1.0/uv.lock +612 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/provably-sdk
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v3
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
33
|
+
with:
|
|
34
|
+
packages-dir: dist
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Business Source License 1.1
|
|
4
|
+
|
|
5
|
+
Copyright © 2026 Provably Technologies LTD
|
|
6
|
+
|
|
7
|
+
Use Limitation: You may not offer the Software as a commercial hosted
|
|
8
|
+
service without purchasing a commercial license from Provably Technologies Ltd.
|
|
9
|
+
|
|
10
|
+
Change Date: 2029-05-07
|
|
11
|
+
Change License: GPL-3.0-or-later
|
|
12
|
+
|
|
13
|
+
Business Source License 1.1
|
|
14
|
+
|
|
15
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
16
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
1. Definitions
|
|
20
|
+
The "Licensed Work" is the software and documentation made available
|
|
21
|
+
under this License.
|
|
22
|
+
The "Licensor" is the entity offering the Licensed Work under this
|
|
23
|
+
License.
|
|
24
|
+
"You" refers to the licensee of the Licensed Work.
|
|
25
|
+
"Use Limitation" means the restrictions (if any) placed on your use of
|
|
26
|
+
the Licensed Work before the Change Date, as listed in the license
|
|
27
|
+
parameters at the top of this License.
|
|
28
|
+
"Additional Use Grant" means additional rights (if any) the Licensor
|
|
29
|
+
grants to you before the Change Date, as listed in the license parameters
|
|
30
|
+
at the top of this License.
|
|
31
|
+
The "Change License" is the license selected by the Licensor to replace
|
|
32
|
+
this License, as listed in the license parameters at the top of this
|
|
33
|
+
License.
|
|
34
|
+
The "Change Date" is the date when the license automatically changes
|
|
35
|
+
from this License to the Change License, as listed in the license
|
|
36
|
+
parameters at the top of this License.
|
|
37
|
+
"Distribution" and "Use" have the same meaning as under copyright law.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
2. License Grant
|
|
41
|
+
From the Effective Date until the Change Date, the Licensor hereby grants
|
|
42
|
+
you the following rights:
|
|
43
|
+
a) The right to copy, modify, create derivative works, redistribute, and
|
|
44
|
+
make non-production Use of the Licensed Work, solely in compliance
|
|
45
|
+
with the Use Limitation and Additional Use Grant.
|
|
46
|
+
b) The right to sublicense the foregoing rights to third parties acting
|
|
47
|
+
on your behalf (for example, cloud service providers or contractors),
|
|
48
|
+
subject to the same Use Limitation.
|
|
49
|
+
|
|
50
|
+
All other rights are expressly reserved by the Licensor.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
3. Change Date
|
|
54
|
+
On the Change Date, the rights granted in Section 2 terminate, and you
|
|
55
|
+
may thereafter exercise the rights granted to you under the Change
|
|
56
|
+
License. The Licensor will publish the Change License in the same
|
|
57
|
+
repository or source as the Licensed Work or in any successor location.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
4. Compliance and Termination
|
|
61
|
+
Any Use of the Licensed Work not expressly permitted under this License
|
|
62
|
+
(including violating the Use Limitation) is **prohibited**.
|
|
63
|
+
|
|
64
|
+
If you violate this License, your rights will terminate automatically,
|
|
65
|
+
and you must cease all Use and Distribution of the Licensed Work as soon
|
|
66
|
+
as possible. Your licenses from anyone else remain in force.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
5. Disclaimer of Warranty
|
|
70
|
+
THE LICENSED WORK IS PROVIDED "AS IS". TO THE MAXIMUM EXTENT PERMITTED
|
|
71
|
+
BY APPLICABLE LAW, THE LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR
|
|
72
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF
|
|
73
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND
|
|
74
|
+
NON-INFRINGEMENT.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
6. Limitation of Liability
|
|
78
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE
|
|
79
|
+
LICENSOR BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR
|
|
80
|
+
CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN CONNECTION WITH THE LICENSED
|
|
81
|
+
WORK OR THIS LICENSE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
7. Trademarks
|
|
85
|
+
This License does not grant permission to use the Licensor's trademarks
|
|
86
|
+
or trade names, except to the extent required for reasonable and
|
|
87
|
+
customary use in describing the origin of the Licensed Work.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
8. Severability
|
|
91
|
+
If any provision of this License is held unenforceable or invalid under
|
|
92
|
+
applicable law, such provision shall be interpreted to accomplish the
|
|
93
|
+
objectives of such provision to the greatest extent possible and the
|
|
94
|
+
remaining provisions shall continue in full force and effect.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
9. Governing Law
|
|
98
|
+
This License shall be governed by and construed in accordance with the
|
|
99
|
+
laws of the jurisdiction specified by the Licensor, excluding its
|
|
100
|
+
conflicts-of-law principles. If no jurisdiction is specified, the laws
|
|
101
|
+
of Finland apply (excluding conflict-of-law rules).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: provably-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Provably API: OAuth sign-in, sandboxes, collections, queries and proofs.
|
|
5
|
+
Project-URL: Homepage, https://provably.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/ProvablyAI/provably-sdk-python
|
|
7
|
+
Author: Provably Technologies Ltd
|
|
8
|
+
License: Business Source License 1.1
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Keywords: oauth,proofs,provably,sdk,verifiable
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: httpx>=0.26
|
|
21
|
+
Requires-Dist: msgspec>=0.18.0
|
|
22
|
+
Requires-Dist: pydantic>=2.6
|
|
23
|
+
Requires-Dist: structlog>=24.1
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# provably-sdk
|
|
32
|
+
|
|
33
|
+
Python SDK for the Provably API: OAuth browser sign-in, sandboxes, middlewares,
|
|
34
|
+
databases, collections, preprocessing, queries and proofs.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install provably-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configure
|
|
41
|
+
|
|
42
|
+
The SDK reads nothing from the environment or disk. Tell it where the API is,
|
|
43
|
+
which organisation to act for and where to keep the session:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import uuid
|
|
47
|
+
|
|
48
|
+
import provably
|
|
49
|
+
from provably import MemoryTokenStore, ProvablyConfig
|
|
50
|
+
|
|
51
|
+
provably.configure(
|
|
52
|
+
config=ProvablyConfig(org_id=uuid.UUID("...")), # api_url / app_url default to production
|
|
53
|
+
tokens=MemoryTokenStore(),
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `config` may be a callable, read whenever a client is built, for apps whose
|
|
58
|
+
settings change at runtime. It must not raise before sign-in; leave `org_id`
|
|
59
|
+
unset and org-scoped calls raise `ProvablyConfigError`.
|
|
60
|
+
- `tokens` is any object with `load()`, `save(tokens)` and
|
|
61
|
+
`clear_refresh_token()` (see `provably.TokenStore`). Rotated tokens are written
|
|
62
|
+
back to it, together with the OAuth client they were issued to.
|
|
63
|
+
- `egress` is an optional context manager every HTTP call runs inside, for apps
|
|
64
|
+
that intercept their own outbound traffic.
|
|
65
|
+
|
|
66
|
+
## Sign in
|
|
67
|
+
|
|
68
|
+
Each app signs in as its own registered OAuth client, on its own loopback port:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
tokens = await provably.browser_login(client_id="my-app", port=8911)
|
|
72
|
+
store.save(tokens)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Use
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from provably import service
|
|
79
|
+
|
|
80
|
+
sandbox = await service.get_sandbox()
|
|
81
|
+
query_id = await service.run_query(middleware_id, collection_id, "SELECT 1")
|
|
82
|
+
await service.wait_for_proof_computation(query_id)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv sync --extra dev
|
|
89
|
+
uv run pytest
|
|
90
|
+
uv run ruff check . && uv run ruff format --check .
|
|
91
|
+
uv run mypy
|
|
92
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# provably-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the Provably API: OAuth browser sign-in, sandboxes, middlewares,
|
|
4
|
+
databases, collections, preprocessing, queries and proofs.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install provably-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Configure
|
|
11
|
+
|
|
12
|
+
The SDK reads nothing from the environment or disk. Tell it where the API is,
|
|
13
|
+
which organisation to act for and where to keep the session:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
import provably
|
|
19
|
+
from provably import MemoryTokenStore, ProvablyConfig
|
|
20
|
+
|
|
21
|
+
provably.configure(
|
|
22
|
+
config=ProvablyConfig(org_id=uuid.UUID("...")), # api_url / app_url default to production
|
|
23
|
+
tokens=MemoryTokenStore(),
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `config` may be a callable, read whenever a client is built, for apps whose
|
|
28
|
+
settings change at runtime. It must not raise before sign-in; leave `org_id`
|
|
29
|
+
unset and org-scoped calls raise `ProvablyConfigError`.
|
|
30
|
+
- `tokens` is any object with `load()`, `save(tokens)` and
|
|
31
|
+
`clear_refresh_token()` (see `provably.TokenStore`). Rotated tokens are written
|
|
32
|
+
back to it, together with the OAuth client they were issued to.
|
|
33
|
+
- `egress` is an optional context manager every HTTP call runs inside, for apps
|
|
34
|
+
that intercept their own outbound traffic.
|
|
35
|
+
|
|
36
|
+
## Sign in
|
|
37
|
+
|
|
38
|
+
Each app signs in as its own registered OAuth client, on its own loopback port:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
tokens = await provably.browser_login(client_id="my-app", port=8911)
|
|
42
|
+
store.save(tokens)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Use
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from provably import service
|
|
49
|
+
|
|
50
|
+
sandbox = await service.get_sandbox()
|
|
51
|
+
query_id = await service.run_query(middleware_id, collection_id, "SELECT 1")
|
|
52
|
+
await service.wait_for_proof_computation(query_id)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv sync --extra dev
|
|
59
|
+
uv run pytest
|
|
60
|
+
uv run ruff check . && uv run ruff format --check .
|
|
61
|
+
uv run mypy
|
|
62
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "provably-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Provably API: OAuth sign-in, sandboxes, collections, queries and proofs."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { text = "Business Source License 1.1" }
|
|
12
|
+
authors = [{ name = "Provably Technologies Ltd" }]
|
|
13
|
+
keywords = ["provably", "sdk", "verifiable", "proofs", "oauth"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.26",
|
|
26
|
+
"msgspec>=0.18.0",
|
|
27
|
+
"pydantic>=2.6",
|
|
28
|
+
"structlog>=24.1",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-asyncio>=0.23",
|
|
35
|
+
"ruff>=0.3",
|
|
36
|
+
"mypy>=1.10",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://provably.ai"
|
|
41
|
+
Repository = "https://github.com/ProvablyAI/provably-sdk-python"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/provably"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 120
|
|
48
|
+
target-version = "py312"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "UP", "W"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.isort]
|
|
54
|
+
known-first-party = ["provably"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
pythonpath = ["src", "."]
|
|
59
|
+
asyncio_mode = "auto"
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.12"
|
|
63
|
+
mypy_path = "src"
|
|
64
|
+
files = ["src", "tests"]
|
|
65
|
+
strict = true
|
|
66
|
+
warn_unreachable = true
|
|
67
|
+
plugins = ["pydantic.mypy"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Python SDK for the Provably API."""
|
|
2
|
+
|
|
3
|
+
from provably._answer_model import AggregateAnswer, QueryAnswer, ResultsetAnswer, TabularData
|
|
4
|
+
from provably._auth_api import Organization, OrganizationType
|
|
5
|
+
from provably._config import (
|
|
6
|
+
MemoryTokenStore,
|
|
7
|
+
OAuthTokens,
|
|
8
|
+
ProvablyConfig,
|
|
9
|
+
ProvablyConfigError,
|
|
10
|
+
TokenStore,
|
|
11
|
+
configure,
|
|
12
|
+
)
|
|
13
|
+
from provably._errors import (
|
|
14
|
+
ProvablyAPIError,
|
|
15
|
+
ProvablyAuthError,
|
|
16
|
+
ProvablyConnectionError,
|
|
17
|
+
ProvablyDataError,
|
|
18
|
+
ProvablyError,
|
|
19
|
+
ProvablyNotFoundError,
|
|
20
|
+
ProvablyResourceAlreadyExistsError,
|
|
21
|
+
ProvablyUnauthorizedError,
|
|
22
|
+
)
|
|
23
|
+
from provably.auth_service import ProvablyAuthService, auth_service
|
|
24
|
+
from provably.connection import ConnectionInfo
|
|
25
|
+
from provably.oauth_login import browser_login
|
|
26
|
+
from provably.service import ProvablyService, service
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AggregateAnswer",
|
|
30
|
+
"ConnectionInfo",
|
|
31
|
+
"MemoryTokenStore",
|
|
32
|
+
"OAuthTokens",
|
|
33
|
+
"Organization",
|
|
34
|
+
"OrganizationType",
|
|
35
|
+
"ProvablyAPIError",
|
|
36
|
+
"ProvablyAuthError",
|
|
37
|
+
"ProvablyAuthService",
|
|
38
|
+
"ProvablyConfig",
|
|
39
|
+
"ProvablyConfigError",
|
|
40
|
+
"ProvablyConnectionError",
|
|
41
|
+
"ProvablyDataError",
|
|
42
|
+
"ProvablyError",
|
|
43
|
+
"ProvablyNotFoundError",
|
|
44
|
+
"ProvablyResourceAlreadyExistsError",
|
|
45
|
+
"ProvablyService",
|
|
46
|
+
"ProvablyUnauthorizedError",
|
|
47
|
+
"QueryAnswer",
|
|
48
|
+
"ResultsetAnswer",
|
|
49
|
+
"TabularData",
|
|
50
|
+
"TokenStore",
|
|
51
|
+
"auth_service",
|
|
52
|
+
"browser_login",
|
|
53
|
+
"configure",
|
|
54
|
+
"service",
|
|
55
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Data validation and serialization contracts for the Rust API layers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
import msgspec
|
|
6
|
+
import structlog
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
_log = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
__all__ = ["QueryAnswer", "TabularData", "AggregateAnswer", "ResultsetAnswer"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _safe_deserialize(cell: Any) -> Any:
|
|
15
|
+
"""Helper to cleanly parse stringified JSON containers using high-speed C decoding."""
|
|
16
|
+
if isinstance(cell, str) and cell.strip().startswith(("{", "[")):
|
|
17
|
+
try:
|
|
18
|
+
return msgspec.json.decode(cell)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
_log.debug("safe_deserialize_fallback", error=str(e))
|
|
21
|
+
return cell
|
|
22
|
+
return cell
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TabularData(BaseModel):
|
|
26
|
+
columns: list[dict[str, Any]] = Field(default_factory=list)
|
|
27
|
+
rows: list[list[Any]] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def extract_value(self) -> Any:
|
|
30
|
+
"""Unpacks tabular results down to the core target payload."""
|
|
31
|
+
if not self.rows:
|
|
32
|
+
return {"columns": self.columns, "rows": self.rows}
|
|
33
|
+
|
|
34
|
+
row0 = self.rows[0]
|
|
35
|
+
# Extracted column names are normalized to lowercase
|
|
36
|
+
col_names = [str(c.get("name") or "").lower() for c in self.columns]
|
|
37
|
+
|
|
38
|
+
# Target 1: Extract intercept record payloads cleanly if present
|
|
39
|
+
if "raw_response" in col_names:
|
|
40
|
+
return _safe_deserialize(row0[col_names.index("raw_response")])
|
|
41
|
+
|
|
42
|
+
# Target 2: Single cell scalar
|
|
43
|
+
if len(col_names) == 1 and len(row0) == 1:
|
|
44
|
+
return _safe_deserialize(row0[0])
|
|
45
|
+
|
|
46
|
+
return {"columns": self.columns, "rows": self.rows}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AggregateAnswer(BaseModel):
|
|
50
|
+
type: Literal["aggregate"]
|
|
51
|
+
value: str
|
|
52
|
+
|
|
53
|
+
def extract_value(self) -> Any:
|
|
54
|
+
return _safe_deserialize(self.value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResultsetAnswer(BaseModel):
|
|
58
|
+
type: Literal["resultset"]
|
|
59
|
+
value: TabularData
|
|
60
|
+
|
|
61
|
+
def extract_value(self) -> Any:
|
|
62
|
+
return self.value.extract_value()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class QueryAnswer(BaseModel):
|
|
66
|
+
"""Wrapper mapping the structure of QueryAnswer enum."""
|
|
67
|
+
|
|
68
|
+
root: AggregateAnswer | ResultsetAnswer = Field(..., discriminator="type")
|
|
69
|
+
|
|
70
|
+
@model_validator(mode="before")
|
|
71
|
+
@classmethod
|
|
72
|
+
def wrap_root(cls, data: Any) -> Any:
|
|
73
|
+
if isinstance(data, dict) and "root" not in data:
|
|
74
|
+
return {"root": data}
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
def flatten(self) -> Any:
|
|
78
|
+
return self.root.extract_value()
|