mini-oss-osdk 0.1.0a3__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.
- mini_oss_osdk-0.1.0a3/LICENSE +13 -0
- mini_oss_osdk-0.1.0a3/MANIFEST.in +1 -0
- mini_oss_osdk-0.1.0a3/PKG-INFO +71 -0
- mini_oss_osdk-0.1.0a3/README.md +49 -0
- mini_oss_osdk-0.1.0a3/pyproject.toml +39 -0
- mini_oss_osdk-0.1.0a3/setup.cfg +4 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/__init__.py +45 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/client.py +285 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/facade.py +295 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/operation.py +94 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/py.typed +0 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/query.py +160 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk/result.py +211 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk.egg-info/PKG-INFO +71 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk.egg-info/SOURCES.txt +24 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk.egg-info/dependency_links.txt +1 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk.egg-info/requires.txt +1 -0
- mini_oss_osdk-0.1.0a3/src/mini_oss_osdk.egg-info/top_level.txt +1 -0
- mini_oss_osdk-0.1.0a3/tests/fixtures/flight-passenger-order/README.md +15 -0
- mini_oss_osdk-0.1.0a3/tests/fixtures/flight-passenger-order/adult-passenger-order-aggregate.query.json +48 -0
- mini_oss_osdk-0.1.0a3/tests/fixtures/flight-passenger-order/operation.succeeded.json +13 -0
- mini_oss_osdk-0.1.0a3/tests/fixtures/flight-passenger-order/result-ref.json +30 -0
- mini_oss_osdk-0.1.0a3/tests/test_contract_fixture.py +32 -0
- mini_oss_osdk-0.1.0a3/tests/test_facade.py +272 -0
- mini_oss_osdk-0.1.0a3/tests/test_operation_client.py +175 -0
- mini_oss_osdk-0.1.0a3/tests/test_result_client.py +140 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2026 ITEM. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its associated documentation are proprietary to ITEM.
|
|
4
|
+
No permission is granted to use, copy, modify, merge, publish, distribute,
|
|
5
|
+
sublicense, sell, or create derivative works from this software except as
|
|
6
|
+
expressly authorized under a separate written agreement with ITEM.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL ITEM
|
|
11
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR
|
|
12
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN THE
|
|
13
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include tests/fixtures *.json *.md
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mini-oss-osdk
|
|
3
|
+
Version: 0.1.0a3
|
|
4
|
+
Summary: Async Python client and object facade for the ITEM Mini-OSS runtime
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
Keywords: item,ontology,osdk,object-query
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: httpx<1,>=0.27
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Mini-OSS Python OSDK
|
|
24
|
+
|
|
25
|
+
Async Python builder/runtime for canonical Object Query, Operation polling and bounded Result Gateway access. `0.1.0a3` adds a unified high-level facade while retaining the low-level `ObjectEngineClient` and `ResultGatewayClient` APIs. Production and isolated Preview Staging API Key lanes have passed page, lookup iterator and high-level `get()` verification.
|
|
26
|
+
|
|
27
|
+
The public PyPI release lane is being prepared as distribution `mini-oss-osdk` with import `mini_oss_osdk`, but it is not published yet. A package name remains unreserved until the first successful registry upload.
|
|
28
|
+
|
|
29
|
+
## High-level object lookup
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from mini_oss_osdk import OSDKClient
|
|
33
|
+
|
|
34
|
+
async with OSDKClient(
|
|
35
|
+
tenant_id="01jabcdefgh1",
|
|
36
|
+
object_engine_url="https://object-engine.example.com",
|
|
37
|
+
result_gateway_url="https://object-results.example.com",
|
|
38
|
+
api_key=tenant_api_key,
|
|
39
|
+
) as client:
|
|
40
|
+
restaurant = await client.ontology.objects.object(
|
|
41
|
+
"01jabcdefgh2", "Restaurant"
|
|
42
|
+
).get(
|
|
43
|
+
"restaurant-001",
|
|
44
|
+
select=("name", "city"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if restaurant is not None:
|
|
48
|
+
print(restaurant.rid, restaurant.primary_key, restaurant["name"])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`get()` only orchestrates canonical lookup → execute → wait → bounded Result page. It never reads Dataset/Redash/Athena metadata or creates SQL. Generic runtime has no Catalog property list, so `select=()` requests identity only; callers list Property API names explicitly. A future generated ontology package may provide that static list.
|
|
52
|
+
|
|
53
|
+
Both service endpoints are required. HTTPS is mandatory except loopback HTTP used by tests. A local wait timeout does not cancel the server Operation.
|
|
54
|
+
|
|
55
|
+
Generated ontology-specific classes are not implemented.
|
|
56
|
+
|
|
57
|
+
## Distribution verification
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m unittest discover -s tests
|
|
61
|
+
python -m ruff check src tests scripts
|
|
62
|
+
python -m build
|
|
63
|
+
python -m twine check dist/*
|
|
64
|
+
python scripts/verify_distribution.py dist
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Branch and `main` pipelines only test and build. A `v<version>` tag is the sole public PyPI upload authority, and the tag must exactly match wheel metadata before upload.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Proprietary. Copyright (c) 2026 ITEM. All rights reserved. See [`LICENSE`](LICENSE); public package availability does not grant permission to use, modify, or redistribute the software.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Mini-OSS Python OSDK
|
|
2
|
+
|
|
3
|
+
Async Python builder/runtime for canonical Object Query, Operation polling and bounded Result Gateway access. `0.1.0a3` adds a unified high-level facade while retaining the low-level `ObjectEngineClient` and `ResultGatewayClient` APIs. Production and isolated Preview Staging API Key lanes have passed page, lookup iterator and high-level `get()` verification.
|
|
4
|
+
|
|
5
|
+
The public PyPI release lane is being prepared as distribution `mini-oss-osdk` with import `mini_oss_osdk`, but it is not published yet. A package name remains unreserved until the first successful registry upload.
|
|
6
|
+
|
|
7
|
+
## High-level object lookup
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from mini_oss_osdk import OSDKClient
|
|
11
|
+
|
|
12
|
+
async with OSDKClient(
|
|
13
|
+
tenant_id="01jabcdefgh1",
|
|
14
|
+
object_engine_url="https://object-engine.example.com",
|
|
15
|
+
result_gateway_url="https://object-results.example.com",
|
|
16
|
+
api_key=tenant_api_key,
|
|
17
|
+
) as client:
|
|
18
|
+
restaurant = await client.ontology.objects.object(
|
|
19
|
+
"01jabcdefgh2", "Restaurant"
|
|
20
|
+
).get(
|
|
21
|
+
"restaurant-001",
|
|
22
|
+
select=("name", "city"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if restaurant is not None:
|
|
26
|
+
print(restaurant.rid, restaurant.primary_key, restaurant["name"])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`get()` only orchestrates canonical lookup → execute → wait → bounded Result page. It never reads Dataset/Redash/Athena metadata or creates SQL. Generic runtime has no Catalog property list, so `select=()` requests identity only; callers list Property API names explicitly. A future generated ontology package may provide that static list.
|
|
30
|
+
|
|
31
|
+
Both service endpoints are required. HTTPS is mandatory except loopback HTTP used by tests. A local wait timeout does not cancel the server Operation.
|
|
32
|
+
|
|
33
|
+
Generated ontology-specific classes are not implemented.
|
|
34
|
+
|
|
35
|
+
## Distribution verification
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m unittest discover -s tests
|
|
39
|
+
python -m ruff check src tests scripts
|
|
40
|
+
python -m build
|
|
41
|
+
python -m twine check dist/*
|
|
42
|
+
python scripts/verify_distribution.py dist
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Branch and `main` pipelines only test and build. A `v<version>` tag is the sole public PyPI upload authority, and the tag must exactly match wheel metadata before upload.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
Proprietary. Copyright (c) 2026 ITEM. All rights reserved. See [`LICENSE`](LICENSE); public package availability does not grant permission to use, modify, or redistribute the software.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mini-oss-osdk"
|
|
7
|
+
version = "0.1.0a3"
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
description = "Async Python client and object facade for the ITEM Mini-OSS runtime"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["item", "ontology", "osdk", "object-query"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
mini_oss_osdk = ["py.typed"]
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py39"
|
|
36
|
+
line-length = 100
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
EngineApiError,
|
|
3
|
+
HttpxTransport,
|
|
4
|
+
ObjectEngineClient,
|
|
5
|
+
OperationWaitTimeout,
|
|
6
|
+
TransportResponse,
|
|
7
|
+
)
|
|
8
|
+
from .facade import (
|
|
9
|
+
ObjectTypeClient,
|
|
10
|
+
OntologyClient,
|
|
11
|
+
OntologyObject,
|
|
12
|
+
OntologyObjects,
|
|
13
|
+
OperationExecutionError,
|
|
14
|
+
OSDKClient,
|
|
15
|
+
OSDKInvariantError,
|
|
16
|
+
)
|
|
17
|
+
from .operation import Operation, OperationError, OperationStatus
|
|
18
|
+
from .query import Field, Metric, ObjectSet, Query
|
|
19
|
+
from .result import ResultApiError, ResultGatewayClient, ResultPage, ResultRef
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"EngineApiError",
|
|
23
|
+
"Field",
|
|
24
|
+
"HttpxTransport",
|
|
25
|
+
"Metric",
|
|
26
|
+
"OSDKClient",
|
|
27
|
+
"OSDKInvariantError",
|
|
28
|
+
"ObjectEngineClient",
|
|
29
|
+
"ObjectTypeClient",
|
|
30
|
+
"OntologyClient",
|
|
31
|
+
"OntologyObject",
|
|
32
|
+
"OntologyObjects",
|
|
33
|
+
"ObjectSet",
|
|
34
|
+
"Operation",
|
|
35
|
+
"OperationError",
|
|
36
|
+
"OperationExecutionError",
|
|
37
|
+
"OperationStatus",
|
|
38
|
+
"OperationWaitTimeout",
|
|
39
|
+
"Query",
|
|
40
|
+
"ResultApiError",
|
|
41
|
+
"ResultGatewayClient",
|
|
42
|
+
"ResultPage",
|
|
43
|
+
"ResultRef",
|
|
44
|
+
"TransportResponse",
|
|
45
|
+
]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import ipaddress
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Awaitable, Callable, Mapping, Optional, Protocol, Union
|
|
8
|
+
from urllib.parse import quote, urlsplit
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .operation import Operation
|
|
13
|
+
from .query import Query
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class TransportResponse:
|
|
18
|
+
status_code: int
|
|
19
|
+
body: Mapping[str, Any]
|
|
20
|
+
headers: Mapping[str, str] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncTransport(Protocol):
|
|
24
|
+
async def request(
|
|
25
|
+
self,
|
|
26
|
+
method: str,
|
|
27
|
+
url: str,
|
|
28
|
+
*,
|
|
29
|
+
headers: Mapping[str, str],
|
|
30
|
+
json_body: Optional[Mapping[str, Any]] = None,
|
|
31
|
+
) -> TransportResponse:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HttpxTransport:
|
|
36
|
+
def __init__(self, client: Optional[httpx.AsyncClient] = None) -> None:
|
|
37
|
+
self._client = client or httpx.AsyncClient()
|
|
38
|
+
self._owns_client = client is None
|
|
39
|
+
|
|
40
|
+
async def request(
|
|
41
|
+
self,
|
|
42
|
+
method: str,
|
|
43
|
+
url: str,
|
|
44
|
+
*,
|
|
45
|
+
headers: Mapping[str, str],
|
|
46
|
+
json_body: Optional[Mapping[str, Any]] = None,
|
|
47
|
+
) -> TransportResponse:
|
|
48
|
+
response = await self._client.request(
|
|
49
|
+
method,
|
|
50
|
+
url,
|
|
51
|
+
headers=dict(headers),
|
|
52
|
+
json=dict(json_body) if json_body is not None else None,
|
|
53
|
+
)
|
|
54
|
+
payload = response.json()
|
|
55
|
+
if not isinstance(payload, Mapping):
|
|
56
|
+
raise ValueError("Object Engine response must be a JSON object")
|
|
57
|
+
return TransportResponse(
|
|
58
|
+
status_code=response.status_code,
|
|
59
|
+
body=dict(payload),
|
|
60
|
+
headers=dict(response.headers),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
if self._owns_client:
|
|
65
|
+
await self._client.aclose()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EngineApiError(RuntimeError):
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
status_code: int,
|
|
73
|
+
code: str,
|
|
74
|
+
message: str,
|
|
75
|
+
retryable: bool,
|
|
76
|
+
request_id: Optional[str],
|
|
77
|
+
details: Mapping[str, Any],
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(f"{code}: {message}")
|
|
80
|
+
self.status_code = status_code
|
|
81
|
+
self.code = code
|
|
82
|
+
self.message = message
|
|
83
|
+
self.retryable = retryable
|
|
84
|
+
self.request_id = request_id
|
|
85
|
+
self.details = dict(details)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class OperationWaitTimeout(TimeoutError):
|
|
89
|
+
def __init__(self, operation: Operation) -> None:
|
|
90
|
+
super().__init__(f"timed out waiting for Operation {operation.operation_id}")
|
|
91
|
+
self.operation = operation
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
QueryInput = Union[Query, Mapping[str, Any]]
|
|
95
|
+
Sleep = Callable[[float], Awaitable[None]]
|
|
96
|
+
Clock = Callable[[], float]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ObjectEngineClient:
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
base_url: str,
|
|
104
|
+
api_key: str,
|
|
105
|
+
transport: Optional[AsyncTransport] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
if not api_key:
|
|
108
|
+
raise ValueError("api_key must not be empty")
|
|
109
|
+
self._base_url = validate_service_base_url(base_url)
|
|
110
|
+
self._api_key = api_key
|
|
111
|
+
self._transport = transport or HttpxTransport()
|
|
112
|
+
|
|
113
|
+
async def aclose(self) -> None:
|
|
114
|
+
close = getattr(self._transport, "aclose", None)
|
|
115
|
+
if close is not None:
|
|
116
|
+
await close()
|
|
117
|
+
|
|
118
|
+
async def execute(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
tenant_id: str,
|
|
122
|
+
namespace_id: str,
|
|
123
|
+
query: QueryInput,
|
|
124
|
+
idempotency_key: str,
|
|
125
|
+
) -> Operation:
|
|
126
|
+
if not idempotency_key:
|
|
127
|
+
raise ValueError("idempotency_key must not be empty")
|
|
128
|
+
response = await self._request(
|
|
129
|
+
"POST",
|
|
130
|
+
self._query_path(tenant_id, namespace_id, ":execute"),
|
|
131
|
+
json_body=_query_wire(query),
|
|
132
|
+
extra_headers={"Idempotency-Key": idempotency_key},
|
|
133
|
+
)
|
|
134
|
+
return Operation.from_wire(response.body)
|
|
135
|
+
|
|
136
|
+
async def get_operation(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
tenant_id: str,
|
|
140
|
+
namespace_id: str,
|
|
141
|
+
operation_id: str,
|
|
142
|
+
) -> Operation:
|
|
143
|
+
response = await self._request(
|
|
144
|
+
"GET",
|
|
145
|
+
self._operation_path(tenant_id, namespace_id, operation_id),
|
|
146
|
+
)
|
|
147
|
+
return Operation.from_wire(response.body)
|
|
148
|
+
|
|
149
|
+
async def cancel(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
tenant_id: str,
|
|
153
|
+
namespace_id: str,
|
|
154
|
+
operation_id: str,
|
|
155
|
+
) -> Operation:
|
|
156
|
+
response = await self._request(
|
|
157
|
+
"POST",
|
|
158
|
+
self._operation_path(tenant_id, namespace_id, operation_id) + ":cancel",
|
|
159
|
+
)
|
|
160
|
+
return Operation.from_wire(response.body)
|
|
161
|
+
|
|
162
|
+
async def wait(
|
|
163
|
+
self,
|
|
164
|
+
operation: Operation,
|
|
165
|
+
*,
|
|
166
|
+
timeout: Optional[float] = None,
|
|
167
|
+
sleep: Sleep = asyncio.sleep,
|
|
168
|
+
monotonic: Clock = time.monotonic,
|
|
169
|
+
) -> Operation:
|
|
170
|
+
if operation.is_terminal:
|
|
171
|
+
return operation
|
|
172
|
+
deadline = monotonic() + timeout if timeout is not None else None
|
|
173
|
+
current = operation
|
|
174
|
+
while not current.is_terminal:
|
|
175
|
+
if deadline is not None and monotonic() >= deadline:
|
|
176
|
+
raise OperationWaitTimeout(current)
|
|
177
|
+
delay = (current.next_poll_after_ms or 1000) / 1000.0
|
|
178
|
+
if deadline is not None:
|
|
179
|
+
delay = min(delay, max(0.0, deadline - monotonic()))
|
|
180
|
+
await sleep(delay)
|
|
181
|
+
if deadline is not None and monotonic() >= deadline:
|
|
182
|
+
raise OperationWaitTimeout(current)
|
|
183
|
+
current = await self.get_operation(
|
|
184
|
+
tenant_id=current.tenant_id,
|
|
185
|
+
namespace_id=current.namespace_id,
|
|
186
|
+
operation_id=current.operation_id,
|
|
187
|
+
)
|
|
188
|
+
return current
|
|
189
|
+
|
|
190
|
+
async def _request(
|
|
191
|
+
self,
|
|
192
|
+
method: str,
|
|
193
|
+
path: str,
|
|
194
|
+
*,
|
|
195
|
+
json_body: Optional[Mapping[str, Any]] = None,
|
|
196
|
+
extra_headers: Optional[Mapping[str, str]] = None,
|
|
197
|
+
) -> TransportResponse:
|
|
198
|
+
headers = {
|
|
199
|
+
"Accept": "application/json",
|
|
200
|
+
"X-API-Key": self._api_key,
|
|
201
|
+
}
|
|
202
|
+
if extra_headers:
|
|
203
|
+
headers.update(extra_headers)
|
|
204
|
+
response = await self._transport.request(
|
|
205
|
+
method,
|
|
206
|
+
self._base_url + path,
|
|
207
|
+
headers=headers,
|
|
208
|
+
json_body=json_body,
|
|
209
|
+
)
|
|
210
|
+
if response.status_code >= 400:
|
|
211
|
+
payload = response.body
|
|
212
|
+
raise EngineApiError(
|
|
213
|
+
status_code=response.status_code,
|
|
214
|
+
code=str(payload.get("code", "ENGINE_HTTP_ERROR")),
|
|
215
|
+
message=str(payload.get("message", "Object Engine request failed")),
|
|
216
|
+
retryable=bool(payload.get("retryable", False)),
|
|
217
|
+
request_id=(
|
|
218
|
+
str(payload["requestId"])
|
|
219
|
+
if payload.get("requestId") is not None
|
|
220
|
+
else None
|
|
221
|
+
),
|
|
222
|
+
details=(
|
|
223
|
+
dict(payload["details"])
|
|
224
|
+
if isinstance(payload.get("details"), Mapping)
|
|
225
|
+
else {}
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
return response
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _query_path(tenant_id: str, namespace_id: str, action: str) -> str:
|
|
232
|
+
return (
|
|
233
|
+
f"/v1/tenants/{quote(tenant_id, safe='')}/namespaces/"
|
|
234
|
+
f"{quote(namespace_id, safe='')}/queries{action}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _operation_path(tenant_id: str, namespace_id: str, operation_id: str) -> str:
|
|
239
|
+
return (
|
|
240
|
+
f"/v1/tenants/{quote(tenant_id, safe='')}/namespaces/"
|
|
241
|
+
f"{quote(namespace_id, safe='')}/operations/"
|
|
242
|
+
f"{quote(operation_id, safe='')}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def validate_service_base_url(base_url: str) -> str:
|
|
247
|
+
if not base_url:
|
|
248
|
+
raise ValueError("service base URL must not be empty")
|
|
249
|
+
parsed = urlsplit(base_url)
|
|
250
|
+
try:
|
|
251
|
+
parsed.port
|
|
252
|
+
except ValueError as error:
|
|
253
|
+
raise ValueError("service base URL has an invalid port") from error
|
|
254
|
+
if (
|
|
255
|
+
not parsed.hostname
|
|
256
|
+
or parsed.username is not None
|
|
257
|
+
or parsed.password is not None
|
|
258
|
+
or parsed.query
|
|
259
|
+
or parsed.fragment
|
|
260
|
+
):
|
|
261
|
+
raise ValueError(
|
|
262
|
+
"service base URL must be absolute and omit credentials, query and fragment"
|
|
263
|
+
)
|
|
264
|
+
if parsed.scheme != "https" and not (
|
|
265
|
+
parsed.scheme == "http" and _is_loopback_host(parsed.hostname)
|
|
266
|
+
):
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"service base URL must use HTTPS; HTTP is only allowed for loopback tests"
|
|
269
|
+
)
|
|
270
|
+
return base_url.rstrip("/")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _is_loopback_host(host: str) -> bool:
|
|
274
|
+
if host.lower() == "localhost":
|
|
275
|
+
return True
|
|
276
|
+
try:
|
|
277
|
+
return ipaddress.ip_address(host).is_loopback
|
|
278
|
+
except ValueError:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _query_wire(query: QueryInput) -> Mapping[str, Any]:
|
|
283
|
+
if isinstance(query, Query):
|
|
284
|
+
return query.to_wire()
|
|
285
|
+
return dict(query)
|