hyperforge-nucliadb 1.0.0.post42__tar.gz → 1.0.0.post58__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.
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/PKG-INFO +1 -1
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/pyproject.toml +1 -1
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/driver.py +2 -2
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/config_driver.py +4 -0
- hyperforge_nucliadb-1.0.0.post58/src/hyperforge_nucliadb/sync/driver.py +254 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/PKG-INFO +1 -1
- hyperforge_nucliadb-1.0.0.post42/src/hyperforge_nucliadb/sync/driver.py +0 -148
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/README.md +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/setup.cfg +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/__init__.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/advanced_ask_agent.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/advanced_ask_config.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/__init__.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/analysis.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/ask.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/config.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/hydrate.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/kb_analysis.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/knowledge_scan.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/models.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/multi.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/nucliadb.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/prompt_analysis.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/query_analysis.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/rerank.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask/utils.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/ask_utils.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/basic_ask_agent.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/basic_ask_config.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/driver_config.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/py.typed +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/__init__.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/agent.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb/sync/config.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/SOURCES.txt +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/dependency_links.txt +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/requires.txt +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/src/hyperforge_nucliadb.egg-info/top_level.txt +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/tests/test_ask_utils.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/tests/test_nucliadb.py +0 -0
- {hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/tests/test_sync.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hyperforge_nucliadb"
|
|
7
|
-
version = "1.0.0.
|
|
7
|
+
version = "1.0.0.post58"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
description = "NucliaDB Hyperforge agent"
|
|
10
10
|
authors = [{ name = "Nuclia", email = "nucliadb@nuclia.com" }]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from asyncio import gather
|
|
3
3
|
from functools import reduce
|
|
4
|
-
from typing import Dict, List, Optional
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
from hyperforge.configure import driver
|
|
@@ -347,7 +347,7 @@ class NucliaDBDriver(Driver):
|
|
|
347
347
|
return await self.driver.catalog(content=q, kbid=self.config.kbid)
|
|
348
348
|
|
|
349
349
|
async def get_resource_by_id(
|
|
350
|
-
self, rid: str, query_params: Optional[Dict[str, str]] = None
|
|
350
|
+
self, rid: str, query_params: Optional[Dict[str, Union[str, List[str]]]] = None
|
|
351
351
|
) -> Optional[Resource]:
|
|
352
352
|
return await self.driver.get_resource_by_id(
|
|
353
353
|
kbid=self.config.kbid, rid=rid, query_params=query_params
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
2
|
|
|
3
|
+
from httpx import Timeout
|
|
3
4
|
from hyperforge.driver import DriverConfig
|
|
4
5
|
from pydantic.config import ConfigDict
|
|
5
6
|
|
|
@@ -8,6 +9,9 @@ from hyperforge_nucliadb.driver_config import (
|
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
SYNC_HTTP_TIMEOUT = Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
class SyncConnection(NucliaDBConnection):
|
|
12
16
|
connection_ids: list[str] # type: ignore
|
|
13
17
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import asyncio
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, cast
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from httpx import (
|
|
10
|
+
AsyncClient,
|
|
11
|
+
ConnectError,
|
|
12
|
+
ConnectTimeout,
|
|
13
|
+
ReadTimeout,
|
|
14
|
+
RemoteProtocolError,
|
|
15
|
+
Response,
|
|
16
|
+
)
|
|
17
|
+
from hyperforge import logger
|
|
18
|
+
from hyperforge.configure import driver
|
|
19
|
+
from hyperforge.interaction import Provider
|
|
20
|
+
from hyperforge.utils.http import SafeTransport
|
|
21
|
+
from nucliadb_models import SyncMetadata
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from hyperforge_nucliadb.driver import (
|
|
25
|
+
NucliaDBDriver,
|
|
26
|
+
connect,
|
|
27
|
+
manager_connect,
|
|
28
|
+
)
|
|
29
|
+
from hyperforge_nucliadb.driver_config import (
|
|
30
|
+
NucliaDBConnection,
|
|
31
|
+
)
|
|
32
|
+
from hyperforge_nucliadb.sync.config_driver import (
|
|
33
|
+
SYNC_HTTP_TIMEOUT,
|
|
34
|
+
SyncConnection,
|
|
35
|
+
SyncDriverConfig,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
SYNC_VALIDATE_ATTEMPTS = 2
|
|
40
|
+
SYNC_VALIDATE_RETRY_BASE_SECONDS = 0.5
|
|
41
|
+
SYNC_VALIDATE_RETRY_JITTER_SECONDS = 0.25
|
|
42
|
+
SYNC_VALIDATE_RETRY_STATUS_CODES = frozenset({502, 503, 504})
|
|
43
|
+
SYNC_VALIDATE_RETRY_EXCEPTIONS = (
|
|
44
|
+
ConnectError,
|
|
45
|
+
ConnectTimeout,
|
|
46
|
+
ReadTimeout,
|
|
47
|
+
RemoteProtocolError,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def sync_connect(conn: SyncConnection):
|
|
52
|
+
headers: Dict[str, str] = {}
|
|
53
|
+
if "http://localhost" in conn.kb_url:
|
|
54
|
+
headers = {
|
|
55
|
+
"X-NUCLIADB-ROLES": "READER",
|
|
56
|
+
}
|
|
57
|
+
else:
|
|
58
|
+
headers = {"AUTHORIZATION": "Bearer " + conn.key} # type: ignore
|
|
59
|
+
return AsyncClient(
|
|
60
|
+
headers=headers,
|
|
61
|
+
base_url=conn.kb_url,
|
|
62
|
+
timeout=SYNC_HTTP_TIMEOUT,
|
|
63
|
+
transport=SafeTransport(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ExternalConnectionOutput(BaseModel):
|
|
68
|
+
"""External connection output without sensitive credential data."""
|
|
69
|
+
|
|
70
|
+
id: UUID
|
|
71
|
+
kb_id: UUID
|
|
72
|
+
created_by: UUID
|
|
73
|
+
created_at: datetime.datetime
|
|
74
|
+
updated_at: datetime.datetime
|
|
75
|
+
provider: Provider
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@driver(
|
|
79
|
+
id="sync",
|
|
80
|
+
title="Sync Source",
|
|
81
|
+
description="Source for interacting with the Sync API.",
|
|
82
|
+
config_schema=SyncDriverConfig,
|
|
83
|
+
)
|
|
84
|
+
class SyncDriver(NucliaDBDriver):
|
|
85
|
+
async_driver: AsyncClient
|
|
86
|
+
config: SyncConnection # type: ignore
|
|
87
|
+
information: Dict[str, ExternalConnectionOutput]
|
|
88
|
+
sync_configs: Dict[str, list[str]]
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
async def init(cls, driver: Any) -> "SyncDriver":
|
|
92
|
+
sync_driver = cast(SyncDriverConfig, driver)
|
|
93
|
+
client = await sync_connect(sync_driver.config)
|
|
94
|
+
information = {}
|
|
95
|
+
sync_configs: Dict[str, list[str]] = {}
|
|
96
|
+
for sync_config_id in sync_driver.config.connection_ids:
|
|
97
|
+
response = await client.get(f"/sync_config/{sync_config_id}")
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
data = response.json()
|
|
100
|
+
connection_id = data["external_connection"]["id"]
|
|
101
|
+
response = await client.get(f"/external_connection/{connection_id}")
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
sync_configs.setdefault(sync_config_id, []).append(connection_id)
|
|
104
|
+
data = response.json()
|
|
105
|
+
information[connection_id] = ExternalConnectionOutput(**data)
|
|
106
|
+
return cls(
|
|
107
|
+
provider=sync_driver.provider,
|
|
108
|
+
name=sync_driver.name,
|
|
109
|
+
async_driver=client,
|
|
110
|
+
information=information,
|
|
111
|
+
sync_configs=sync_configs,
|
|
112
|
+
config=sync_driver.config,
|
|
113
|
+
driver=await connect(cast(NucliaDBConnection, sync_driver.config)),
|
|
114
|
+
manager=await manager_connect(cast(NucliaDBConnection, sync_driver.config)),
|
|
115
|
+
_synonyms=None,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def get_oauth_url(
|
|
119
|
+
self,
|
|
120
|
+
sync_config: str,
|
|
121
|
+
rao_redirect_url: str,
|
|
122
|
+
oauth_uuid: str,
|
|
123
|
+
connection_id: str,
|
|
124
|
+
question_id: str,
|
|
125
|
+
) -> str:
|
|
126
|
+
sep = "&" if "?" in rao_redirect_url else "?"
|
|
127
|
+
rao_redirect_url = (
|
|
128
|
+
f"{rao_redirect_url}{sep}{urlencode({'question_id': question_id})}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
response = await self.async_driver.post(
|
|
132
|
+
f"{self.config.kb_url}/sync_config/{sync_config}/authorize",
|
|
133
|
+
json={
|
|
134
|
+
"connection_id": connection_id,
|
|
135
|
+
"rao_redirect_url": rao_redirect_url,
|
|
136
|
+
"oauth_uuid": oauth_uuid,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
data = response.json()
|
|
141
|
+
return data["authorize_url"]
|
|
142
|
+
|
|
143
|
+
async def validate_resources(
|
|
144
|
+
self,
|
|
145
|
+
resource_ids: List[str],
|
|
146
|
+
credentials: str,
|
|
147
|
+
connection_id: str,
|
|
148
|
+
sync_config_id: str,
|
|
149
|
+
sync_metadata_by_resource: Dict[str, SyncMetadata],
|
|
150
|
+
) -> List[str]:
|
|
151
|
+
path = f"/sync_config/{sync_config_id}/validate_resources"
|
|
152
|
+
payload = {
|
|
153
|
+
"resources": [x.model_dump() for x in sync_metadata_by_resource.values()],
|
|
154
|
+
"credentials": credentials,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
response: Response | None = None
|
|
158
|
+
attempt = 0
|
|
159
|
+
overall_started_at = time.monotonic()
|
|
160
|
+
for attempt in range(1, SYNC_VALIDATE_ATTEMPTS + 1):
|
|
161
|
+
attempt_started_at = time.monotonic()
|
|
162
|
+
try:
|
|
163
|
+
response = await self.async_driver.post(path, json=payload)
|
|
164
|
+
if (
|
|
165
|
+
response.status_code not in SYNC_VALIDATE_RETRY_STATUS_CODES
|
|
166
|
+
or attempt == SYNC_VALIDATE_ATTEMPTS
|
|
167
|
+
):
|
|
168
|
+
if response.status_code in SYNC_VALIDATE_RETRY_STATUS_CODES:
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Sync resource validation got retriable status but reached max attempts",
|
|
171
|
+
extra={
|
|
172
|
+
"sync_config_id": sync_config_id,
|
|
173
|
+
"connection_id": connection_id,
|
|
174
|
+
"resource_count": len(resource_ids),
|
|
175
|
+
"attempt": attempt,
|
|
176
|
+
"duration_seconds": time.monotonic()
|
|
177
|
+
- attempt_started_at,
|
|
178
|
+
"status_code": response.status_code,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
logger.info(
|
|
183
|
+
"Sync resource validation received non-retriable status",
|
|
184
|
+
extra={
|
|
185
|
+
"sync_config_id": sync_config_id,
|
|
186
|
+
"connection_id": connection_id,
|
|
187
|
+
"resource_count": len(resource_ids),
|
|
188
|
+
"attempt": attempt,
|
|
189
|
+
"duration_seconds": time.monotonic()
|
|
190
|
+
- attempt_started_at,
|
|
191
|
+
"status_code": response.status_code,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
break
|
|
195
|
+
error_type = f"HTTP {response.status_code}"
|
|
196
|
+
except SYNC_VALIDATE_RETRY_EXCEPTIONS as exc:
|
|
197
|
+
if attempt == SYNC_VALIDATE_ATTEMPTS:
|
|
198
|
+
logger.warning(
|
|
199
|
+
"Sync resource validation failed after retries",
|
|
200
|
+
extra={
|
|
201
|
+
"sync_config_id": sync_config_id,
|
|
202
|
+
"connection_id": connection_id,
|
|
203
|
+
"resource_count": len(resource_ids),
|
|
204
|
+
"attempt": attempt,
|
|
205
|
+
"duration_seconds": time.monotonic() - attempt_started_at,
|
|
206
|
+
"exception_type": type(exc).__name__,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
raise
|
|
210
|
+
error_type = type(exc).__name__
|
|
211
|
+
|
|
212
|
+
logger.warning(
|
|
213
|
+
"Transient Sync resource validation failure, retrying",
|
|
214
|
+
extra={
|
|
215
|
+
"sync_config_id": sync_config_id,
|
|
216
|
+
"connection_id": connection_id,
|
|
217
|
+
"resource_count": len(resource_ids),
|
|
218
|
+
"attempt": attempt,
|
|
219
|
+
"duration_seconds": time.monotonic() - attempt_started_at,
|
|
220
|
+
"exception_type": error_type,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
delay = SYNC_VALIDATE_RETRY_BASE_SECONDS + random.uniform(
|
|
224
|
+
0, SYNC_VALIDATE_RETRY_JITTER_SECONDS
|
|
225
|
+
)
|
|
226
|
+
await asyncio.sleep(delay)
|
|
227
|
+
|
|
228
|
+
if response is None:
|
|
229
|
+
raise RuntimeError("Sync resource validation made no request attempts")
|
|
230
|
+
|
|
231
|
+
response.raise_for_status()
|
|
232
|
+
logger.info(
|
|
233
|
+
"Sync resource validation completed",
|
|
234
|
+
extra={
|
|
235
|
+
"sync_config_id": sync_config_id,
|
|
236
|
+
"connection_id": connection_id,
|
|
237
|
+
"resource_count": len(resource_ids),
|
|
238
|
+
"attempt": attempt,
|
|
239
|
+
"duration_seconds": time.monotonic() - overall_started_at,
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
data = response.json()
|
|
243
|
+
# The API validates by file_id (origin storage identifier) and returns those back,
|
|
244
|
+
# but resource_filters in AskRequest requires NucliaDB resource UUIDs, which are
|
|
245
|
+
# the keys of sync_metadata_by_resource.
|
|
246
|
+
file_id_to_resource_uuid = {
|
|
247
|
+
metadata.file_id: resource_uuid
|
|
248
|
+
for resource_uuid, metadata in sync_metadata_by_resource.items()
|
|
249
|
+
}
|
|
250
|
+
return [
|
|
251
|
+
file_id_to_resource_uuid[resource["file_id"]]
|
|
252
|
+
for resource in data.get("valid_resources", [])
|
|
253
|
+
if resource["file_id"] in file_id_to_resource_uuid
|
|
254
|
+
]
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
from typing import Any, Dict, List, cast
|
|
3
|
-
from urllib.parse import urlencode
|
|
4
|
-
from uuid import UUID
|
|
5
|
-
|
|
6
|
-
from httpx import AsyncClient
|
|
7
|
-
from hyperforge.configure import driver
|
|
8
|
-
from hyperforge.interaction import Provider
|
|
9
|
-
from hyperforge.utils.http import SafeTransport
|
|
10
|
-
from nucliadb_models import SyncMetadata
|
|
11
|
-
from pydantic import BaseModel
|
|
12
|
-
|
|
13
|
-
from hyperforge_nucliadb.driver import (
|
|
14
|
-
NucliaDBDriver,
|
|
15
|
-
connect,
|
|
16
|
-
manager_connect,
|
|
17
|
-
)
|
|
18
|
-
from hyperforge_nucliadb.driver_config import (
|
|
19
|
-
NucliaDBConnection,
|
|
20
|
-
)
|
|
21
|
-
from hyperforge_nucliadb.sync.config_driver import (
|
|
22
|
-
SyncConnection,
|
|
23
|
-
SyncDriverConfig,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def sync_connect(conn: SyncConnection):
|
|
28
|
-
headers: Dict[str, str] = {}
|
|
29
|
-
if "http://localhost" in conn.kb_url:
|
|
30
|
-
headers = {
|
|
31
|
-
"X-NUCLIADB-ROLES": "READER",
|
|
32
|
-
}
|
|
33
|
-
else:
|
|
34
|
-
headers = {"AUTHORIZATION": "Bearer " + conn.key} # type: ignore
|
|
35
|
-
return AsyncClient(
|
|
36
|
-
headers=headers,
|
|
37
|
-
base_url=conn.kb_url,
|
|
38
|
-
transport=SafeTransport(),
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class ExternalConnectionOutput(BaseModel):
|
|
43
|
-
"""External connection output without sensitive credential data."""
|
|
44
|
-
|
|
45
|
-
id: UUID
|
|
46
|
-
kb_id: UUID
|
|
47
|
-
created_by: UUID
|
|
48
|
-
created_at: datetime.datetime
|
|
49
|
-
updated_at: datetime.datetime
|
|
50
|
-
provider: Provider
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@driver(
|
|
54
|
-
id="sync",
|
|
55
|
-
title="Sync Source",
|
|
56
|
-
description="Source for interacting with the Sync API.",
|
|
57
|
-
config_schema=SyncDriverConfig,
|
|
58
|
-
)
|
|
59
|
-
class SyncDriver(NucliaDBDriver):
|
|
60
|
-
async_driver: AsyncClient
|
|
61
|
-
config: SyncConnection # type: ignore
|
|
62
|
-
information: Dict[str, ExternalConnectionOutput]
|
|
63
|
-
sync_configs: Dict[str, list[str]]
|
|
64
|
-
|
|
65
|
-
@classmethod
|
|
66
|
-
async def init(cls, driver: Any) -> "SyncDriver":
|
|
67
|
-
sync_driver = cast(SyncDriverConfig, driver)
|
|
68
|
-
client = await sync_connect(sync_driver.config)
|
|
69
|
-
information = {}
|
|
70
|
-
sync_configs: Dict[str, list[str]] = {}
|
|
71
|
-
for sync_config_id in sync_driver.config.connection_ids:
|
|
72
|
-
response = await client.get(f"/sync_config/{sync_config_id}")
|
|
73
|
-
response.raise_for_status()
|
|
74
|
-
data = response.json()
|
|
75
|
-
connection_id = data["external_connection"]["id"]
|
|
76
|
-
response = await client.get(f"/external_connection/{connection_id}")
|
|
77
|
-
response.raise_for_status()
|
|
78
|
-
sync_configs.setdefault(sync_config_id, []).append(connection_id)
|
|
79
|
-
data = response.json()
|
|
80
|
-
information[connection_id] = ExternalConnectionOutput(**data)
|
|
81
|
-
return cls(
|
|
82
|
-
provider=sync_driver.provider,
|
|
83
|
-
name=sync_driver.name,
|
|
84
|
-
async_driver=client,
|
|
85
|
-
information=information,
|
|
86
|
-
sync_configs=sync_configs,
|
|
87
|
-
config=sync_driver.config,
|
|
88
|
-
driver=await connect(cast(NucliaDBConnection, sync_driver.config)),
|
|
89
|
-
manager=await manager_connect(cast(NucliaDBConnection, sync_driver.config)),
|
|
90
|
-
_synonyms=None,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
async def get_oauth_url(
|
|
94
|
-
self,
|
|
95
|
-
sync_config: str,
|
|
96
|
-
rao_redirect_url: str,
|
|
97
|
-
oauth_uuid: str,
|
|
98
|
-
connection_id: str,
|
|
99
|
-
question_id: str,
|
|
100
|
-
) -> str:
|
|
101
|
-
sep = "&" if "?" in rao_redirect_url else "?"
|
|
102
|
-
rao_redirect_url = (
|
|
103
|
-
f"{rao_redirect_url}{sep}{urlencode({'question_id': question_id})}"
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
response = await self.async_driver.post(
|
|
107
|
-
f"{self.config.kb_url}/sync_config/{sync_config}/authorize",
|
|
108
|
-
json={
|
|
109
|
-
"connection_id": connection_id,
|
|
110
|
-
"rao_redirect_url": rao_redirect_url,
|
|
111
|
-
"oauth_uuid": oauth_uuid,
|
|
112
|
-
},
|
|
113
|
-
)
|
|
114
|
-
response.raise_for_status()
|
|
115
|
-
data = response.json()
|
|
116
|
-
return data["authorize_url"]
|
|
117
|
-
|
|
118
|
-
async def validate_resources(
|
|
119
|
-
self,
|
|
120
|
-
resource_ids: List[str],
|
|
121
|
-
credentials: str,
|
|
122
|
-
connection_id: str,
|
|
123
|
-
sync_config_id: str,
|
|
124
|
-
sync_metadata_by_resource: Dict[str, SyncMetadata],
|
|
125
|
-
) -> List[str]:
|
|
126
|
-
response = await self.async_driver.post(
|
|
127
|
-
f"/sync_config/{sync_config_id}/validate_resources",
|
|
128
|
-
json={
|
|
129
|
-
"resources": [
|
|
130
|
-
x.model_dump() for x in sync_metadata_by_resource.values()
|
|
131
|
-
],
|
|
132
|
-
"credentials": credentials,
|
|
133
|
-
},
|
|
134
|
-
)
|
|
135
|
-
response.raise_for_status()
|
|
136
|
-
data = response.json()
|
|
137
|
-
# The API validates by file_id (origin storage identifier) and returns those back,
|
|
138
|
-
# but resource_filters in AskRequest requires NucliaDB resource UUIDs, which are
|
|
139
|
-
# the keys of sync_metadata_by_resource.
|
|
140
|
-
file_id_to_resource_uuid = {
|
|
141
|
-
metadata.file_id: resource_uuid
|
|
142
|
-
for resource_uuid, metadata in sync_metadata_by_resource.items()
|
|
143
|
-
}
|
|
144
|
-
return [
|
|
145
|
-
file_id_to_resource_uuid[resource["file_id"]]
|
|
146
|
-
for resource in data.get("valid_resources", [])
|
|
147
|
-
if resource["file_id"] in file_id_to_resource_uuid
|
|
148
|
-
]
|
|
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
|
|
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
|
{hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/tests/test_ask_utils.py
RENAMED
|
File without changes
|
{hyperforge_nucliadb-1.0.0.post42 → hyperforge_nucliadb-1.0.0.post58}/tests/test_nucliadb.py
RENAMED
|
File without changes
|
|
File without changes
|