inputlayer-client-dev 0.1.0__py3-none-any.whl
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.
- inputlayer/__init__.py +151 -0
- inputlayer/_ast.py +133 -0
- inputlayer/_naming.py +44 -0
- inputlayer/_protocol.py +225 -0
- inputlayer/_proxy.py +244 -0
- inputlayer/aggregations.py +126 -0
- inputlayer/auth.py +70 -0
- inputlayer/client.py +178 -0
- inputlayer/client_sync.py +231 -0
- inputlayer/compiler.py +672 -0
- inputlayer/connection.py +314 -0
- inputlayer/derived.py +141 -0
- inputlayer/exceptions.py +76 -0
- inputlayer/functions.py +285 -0
- inputlayer/index.py +40 -0
- inputlayer/knowledge_graph.py +589 -0
- inputlayer/migrations/__init__.py +51 -0
- inputlayer/migrations/autodetector.py +120 -0
- inputlayer/migrations/cli.py +254 -0
- inputlayer/migrations/executor.py +95 -0
- inputlayer/migrations/loader.py +91 -0
- inputlayer/migrations/operations.py +298 -0
- inputlayer/migrations/recorder.py +44 -0
- inputlayer/migrations/state.py +107 -0
- inputlayer/migrations/writer.py +183 -0
- inputlayer/notifications.py +83 -0
- inputlayer/py.typed +0 -0
- inputlayer/relation.py +102 -0
- inputlayer/result.py +89 -0
- inputlayer/session.py +75 -0
- inputlayer/types.py +192 -0
- inputlayer_client_dev-0.1.0.dist-info/METADATA +364 -0
- inputlayer_client_dev-0.1.0.dist-info/RECORD +35 -0
- inputlayer_client_dev-0.1.0.dist-info/WHEEL +4 -0
- inputlayer_client_dev-0.1.0.dist-info/entry_points.txt +2 -0
inputlayer/connection.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""WebSocket connection management with authentication and streaming support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import websockets
|
|
10
|
+
from websockets.asyncio.client import ClientConnection
|
|
11
|
+
|
|
12
|
+
from inputlayer._protocol import (
|
|
13
|
+
AuthenticateMessage,
|
|
14
|
+
AuthenticatedResponse,
|
|
15
|
+
AuthErrorResponse,
|
|
16
|
+
ErrorResponse,
|
|
17
|
+
ExecuteMessage,
|
|
18
|
+
LoginMessage,
|
|
19
|
+
NotificationResponse,
|
|
20
|
+
PingMessage,
|
|
21
|
+
PongResponse,
|
|
22
|
+
ResultChunkResponse,
|
|
23
|
+
ResultEndResponse,
|
|
24
|
+
ResultResponse,
|
|
25
|
+
ResultStartResponse,
|
|
26
|
+
ServerMessage,
|
|
27
|
+
deserialize_message,
|
|
28
|
+
)
|
|
29
|
+
from inputlayer.exceptions import (
|
|
30
|
+
AuthenticationError,
|
|
31
|
+
ConnectionError,
|
|
32
|
+
InternalError,
|
|
33
|
+
)
|
|
34
|
+
from inputlayer.notifications import NotificationDispatcher, NotificationEvent
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("inputlayer")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Connection:
|
|
40
|
+
"""Manages the WebSocket connection to an InputLayer server."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
url: str,
|
|
45
|
+
*,
|
|
46
|
+
username: str | None = None,
|
|
47
|
+
password: str | None = None,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
auto_reconnect: bool = True,
|
|
50
|
+
reconnect_delay: float = 1.0,
|
|
51
|
+
max_reconnect_attempts: int = 10,
|
|
52
|
+
initial_kg: str | None = None,
|
|
53
|
+
last_seq: int | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._url = url
|
|
56
|
+
self._username = username
|
|
57
|
+
self._password = password
|
|
58
|
+
self._api_key = api_key
|
|
59
|
+
self._auto_reconnect = auto_reconnect
|
|
60
|
+
self._reconnect_delay = reconnect_delay
|
|
61
|
+
self._max_reconnect_attempts = max_reconnect_attempts
|
|
62
|
+
self._initial_kg = initial_kg
|
|
63
|
+
self._last_seq = last_seq
|
|
64
|
+
|
|
65
|
+
self._ws: ClientConnection | None = None
|
|
66
|
+
self._session_id: str | None = None
|
|
67
|
+
self._server_version: str | None = None
|
|
68
|
+
self._role: str | None = None
|
|
69
|
+
self._current_kg: str | None = None
|
|
70
|
+
self._connected = False
|
|
71
|
+
|
|
72
|
+
self._dispatcher = NotificationDispatcher()
|
|
73
|
+
self._recv_task: asyncio.Task | None = None
|
|
74
|
+
|
|
75
|
+
# ── Properties ────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def connected(self) -> bool:
|
|
79
|
+
return self._connected
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def session_id(self) -> str | None:
|
|
83
|
+
return self._session_id
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def server_version(self) -> str | None:
|
|
87
|
+
return self._server_version
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def role(self) -> str | None:
|
|
91
|
+
return self._role
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def current_kg(self) -> str | None:
|
|
95
|
+
return self._current_kg
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def dispatcher(self) -> NotificationDispatcher:
|
|
99
|
+
return self._dispatcher
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def last_seq(self) -> int:
|
|
103
|
+
return self._dispatcher.last_seq
|
|
104
|
+
|
|
105
|
+
# ── Connection lifecycle ──────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async def connect(self) -> None:
|
|
108
|
+
"""Connect and authenticate."""
|
|
109
|
+
ws_url = self._url
|
|
110
|
+
params = []
|
|
111
|
+
if self._initial_kg:
|
|
112
|
+
params.append(f"kg={self._initial_kg}")
|
|
113
|
+
if self._last_seq is not None:
|
|
114
|
+
params.append(f"last_seq={self._last_seq}")
|
|
115
|
+
if params:
|
|
116
|
+
separator = "&" if "?" in ws_url else "?"
|
|
117
|
+
ws_url = f"{ws_url}{separator}{'&'.join(params)}"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
self._ws = await websockets.connect(ws_url)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise ConnectionError(f"Failed to connect to {ws_url}: {e}") from e
|
|
123
|
+
|
|
124
|
+
await self._authenticate()
|
|
125
|
+
self._connected = True
|
|
126
|
+
|
|
127
|
+
# Start background receiver for notifications
|
|
128
|
+
self._recv_task = asyncio.create_task(self._receive_loop())
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
"""Close the connection gracefully."""
|
|
132
|
+
self._connected = False
|
|
133
|
+
if self._recv_task and not self._recv_task.done():
|
|
134
|
+
self._recv_task.cancel()
|
|
135
|
+
try:
|
|
136
|
+
await self._recv_task
|
|
137
|
+
except asyncio.CancelledError:
|
|
138
|
+
pass
|
|
139
|
+
if self._ws:
|
|
140
|
+
try:
|
|
141
|
+
await self._ws.close()
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
self._ws = None
|
|
145
|
+
|
|
146
|
+
async def _authenticate(self) -> None:
|
|
147
|
+
"""Send authentication message and wait for response."""
|
|
148
|
+
assert self._ws is not None
|
|
149
|
+
|
|
150
|
+
if self._api_key:
|
|
151
|
+
msg = AuthenticateMessage(api_key=self._api_key)
|
|
152
|
+
elif self._username and self._password:
|
|
153
|
+
msg = LoginMessage(username=self._username, password=self._password)
|
|
154
|
+
else:
|
|
155
|
+
raise AuthenticationError("No credentials provided (need username/password or api_key)")
|
|
156
|
+
|
|
157
|
+
await self._ws.send(msg.to_json())
|
|
158
|
+
raw = await self._ws.recv()
|
|
159
|
+
response = deserialize_message(raw)
|
|
160
|
+
|
|
161
|
+
if isinstance(response, AuthErrorResponse):
|
|
162
|
+
raise AuthenticationError(response.message)
|
|
163
|
+
if isinstance(response, AuthenticatedResponse):
|
|
164
|
+
self._session_id = response.session_id
|
|
165
|
+
self._server_version = response.version
|
|
166
|
+
self._role = response.role
|
|
167
|
+
self._current_kg = response.knowledge_graph
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
raise AuthenticationError(f"Unexpected auth response: {response!r}")
|
|
171
|
+
|
|
172
|
+
# ── Command execution ─────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async def execute(self, program: str) -> ResultResponse:
|
|
175
|
+
"""Send a program/command and wait for the result.
|
|
176
|
+
|
|
177
|
+
Transparently assembles streamed results (result_start → chunks → result_end).
|
|
178
|
+
"""
|
|
179
|
+
if not self._connected or not self._ws:
|
|
180
|
+
raise ConnectionError("Not connected")
|
|
181
|
+
|
|
182
|
+
msg = ExecuteMessage(program=program)
|
|
183
|
+
await self._ws.send(msg.to_json())
|
|
184
|
+
|
|
185
|
+
return await self._read_result()
|
|
186
|
+
|
|
187
|
+
async def _read_result(self) -> ResultResponse:
|
|
188
|
+
"""Read messages until we get a complete result, dispatching notifications."""
|
|
189
|
+
assert self._ws is not None
|
|
190
|
+
while True:
|
|
191
|
+
raw = await self._ws.recv()
|
|
192
|
+
response = deserialize_message(raw)
|
|
193
|
+
|
|
194
|
+
if isinstance(response, NotificationResponse):
|
|
195
|
+
self._dispatch_notification(response)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if isinstance(response, PongResponse):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if isinstance(response, ResultResponse):
|
|
202
|
+
if response.switched_kg:
|
|
203
|
+
self._current_kg = response.switched_kg
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
if isinstance(response, ErrorResponse):
|
|
207
|
+
return ResultResponse(
|
|
208
|
+
columns=["error"],
|
|
209
|
+
rows=[[response.message]],
|
|
210
|
+
row_count=1,
|
|
211
|
+
total_count=1,
|
|
212
|
+
truncated=False,
|
|
213
|
+
execution_time_ms=0,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if isinstance(response, ResultStartResponse):
|
|
217
|
+
return await self._assemble_stream(response)
|
|
218
|
+
|
|
219
|
+
raise InternalError(f"Unexpected message during result read: {response!r}")
|
|
220
|
+
|
|
221
|
+
async def _assemble_stream(self, start: ResultStartResponse) -> ResultResponse:
|
|
222
|
+
"""Assemble a streamed result from chunks."""
|
|
223
|
+
assert self._ws is not None
|
|
224
|
+
all_rows: list[list[Any]] = []
|
|
225
|
+
all_provenance: list[str] = []
|
|
226
|
+
|
|
227
|
+
while True:
|
|
228
|
+
raw = await self._ws.recv()
|
|
229
|
+
response = deserialize_message(raw)
|
|
230
|
+
|
|
231
|
+
if isinstance(response, NotificationResponse):
|
|
232
|
+
self._dispatch_notification(response)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if isinstance(response, ResultChunkResponse):
|
|
236
|
+
all_rows.extend(response.rows)
|
|
237
|
+
if response.row_provenance:
|
|
238
|
+
all_provenance.extend(response.row_provenance)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if isinstance(response, ResultEndResponse):
|
|
242
|
+
if start.switched_kg:
|
|
243
|
+
self._current_kg = start.switched_kg
|
|
244
|
+
return ResultResponse(
|
|
245
|
+
columns=start.columns,
|
|
246
|
+
rows=all_rows,
|
|
247
|
+
row_count=response.row_count,
|
|
248
|
+
total_count=start.total_count,
|
|
249
|
+
truncated=start.truncated,
|
|
250
|
+
execution_time_ms=start.execution_time_ms,
|
|
251
|
+
row_provenance=all_provenance or None,
|
|
252
|
+
metadata=start.metadata,
|
|
253
|
+
switched_kg=start.switched_kg,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
raise InternalError(f"Unexpected message during streaming: {response!r}")
|
|
257
|
+
|
|
258
|
+
# ── Notification handling ─────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def _dispatch_notification(self, notif: NotificationResponse) -> None:
|
|
261
|
+
event = NotificationEvent(
|
|
262
|
+
type=notif.type,
|
|
263
|
+
seq=notif.seq,
|
|
264
|
+
timestamp_ms=notif.timestamp_ms,
|
|
265
|
+
session_id=notif.session_id,
|
|
266
|
+
knowledge_graph=notif.knowledge_graph,
|
|
267
|
+
relation=notif.relation,
|
|
268
|
+
operation=notif.operation,
|
|
269
|
+
count=notif.count,
|
|
270
|
+
rule_name=notif.rule_name,
|
|
271
|
+
entity=notif.entity,
|
|
272
|
+
)
|
|
273
|
+
self._dispatcher.dispatch(event)
|
|
274
|
+
|
|
275
|
+
async def _receive_loop(self) -> None:
|
|
276
|
+
"""Background task that receives notifications when idle."""
|
|
277
|
+
assert self._ws is not None
|
|
278
|
+
try:
|
|
279
|
+
async for raw in self._ws:
|
|
280
|
+
try:
|
|
281
|
+
response = deserialize_message(raw)
|
|
282
|
+
if isinstance(response, NotificationResponse):
|
|
283
|
+
self._dispatch_notification(response)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
except asyncio.CancelledError:
|
|
287
|
+
pass
|
|
288
|
+
except Exception:
|
|
289
|
+
self._connected = False
|
|
290
|
+
if self._auto_reconnect:
|
|
291
|
+
await self._reconnect()
|
|
292
|
+
|
|
293
|
+
async def _reconnect(self) -> None:
|
|
294
|
+
"""Attempt reconnection with exponential backoff."""
|
|
295
|
+
delay = self._reconnect_delay
|
|
296
|
+
for attempt in range(self._max_reconnect_attempts):
|
|
297
|
+
logger.info(f"Reconnecting (attempt {attempt + 1}/{self._max_reconnect_attempts})...")
|
|
298
|
+
await asyncio.sleep(delay)
|
|
299
|
+
try:
|
|
300
|
+
self._last_seq = self._dispatcher.last_seq
|
|
301
|
+
await self.connect()
|
|
302
|
+
logger.info("Reconnected successfully")
|
|
303
|
+
return
|
|
304
|
+
except Exception:
|
|
305
|
+
delay = min(delay * 2, 60.0)
|
|
306
|
+
raise ConnectionError(
|
|
307
|
+
f"Failed to reconnect after {self._max_reconnect_attempts} attempts"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def ping(self) -> None:
|
|
311
|
+
"""Send a keep-alive ping."""
|
|
312
|
+
if not self._ws:
|
|
313
|
+
raise ConnectionError("Not connected")
|
|
314
|
+
await self._ws.send(PingMessage().to_json())
|
inputlayer/derived.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Derived relations and the From/Where/Select rule builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from inputlayer._ast import BoolExpr, Expr
|
|
9
|
+
from inputlayer._proxy import ColumnProxy, RelationProxy, RelationRef
|
|
10
|
+
from inputlayer.relation import Relation
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Derived(Relation):
|
|
17
|
+
"""Base class for derived (rule-computed) relations.
|
|
18
|
+
|
|
19
|
+
Subclass with ``rules: ClassVar[list[RuleClause]]``::
|
|
20
|
+
|
|
21
|
+
class Reachable(Derived):
|
|
22
|
+
src: int
|
|
23
|
+
dst: int
|
|
24
|
+
|
|
25
|
+
rules = [
|
|
26
|
+
From(Edge).select(src=Edge.x, dst=Edge.y),
|
|
27
|
+
From(Reachable, Edge)
|
|
28
|
+
.where(lambda r, e: r.dst == e.x)
|
|
29
|
+
.select(src=Reachable.src, dst=Edge.y),
|
|
30
|
+
]
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
rules: ClassVar[list[RuleClause]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class RuleClause:
|
|
38
|
+
"""A single compiled rule clause: head column map + body relations + condition."""
|
|
39
|
+
|
|
40
|
+
relations: list[tuple[str, type[Relation], str | None]] # (name, cls, alias)
|
|
41
|
+
select_map: dict[str, Expr] # head_column → body Expr
|
|
42
|
+
condition: BoolExpr | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class _FromBase:
|
|
47
|
+
"""Internal: holds the relations for a From(...) builder."""
|
|
48
|
+
|
|
49
|
+
_relations: list[tuple[str, type[Relation], str | None]]
|
|
50
|
+
|
|
51
|
+
def _build_proxy_args(self) -> list[RelationProxy]:
|
|
52
|
+
"""Build proxy objects matching the From(...) arguments."""
|
|
53
|
+
proxies = []
|
|
54
|
+
for rn, cls, alias in self._relations:
|
|
55
|
+
proxies.append(RelationProxy(rn, ref_alias=alias))
|
|
56
|
+
return proxies
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FromWhere(_FromBase):
|
|
60
|
+
"""Intermediate builder after .where() - only .select() remains."""
|
|
61
|
+
|
|
62
|
+
_condition: BoolExpr
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
relations: list[tuple[str, type[Relation], str | None]],
|
|
67
|
+
condition: BoolExpr,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._relations = relations
|
|
70
|
+
self._condition = condition
|
|
71
|
+
|
|
72
|
+
def select(self, **columns: ColumnProxy | Expr) -> RuleClause:
|
|
73
|
+
"""Map derived columns to body expressions.
|
|
74
|
+
|
|
75
|
+
Keyword argument names must match the Derived class field names.
|
|
76
|
+
"""
|
|
77
|
+
select_map: dict[str, Expr] = {}
|
|
78
|
+
for name, val in columns.items():
|
|
79
|
+
if isinstance(val, ColumnProxy):
|
|
80
|
+
select_map[name] = val._to_ast()
|
|
81
|
+
elif isinstance(val, Expr):
|
|
82
|
+
select_map[name] = val
|
|
83
|
+
else:
|
|
84
|
+
raise TypeError(
|
|
85
|
+
f"select() value for '{name}' must be a Column or Expr, "
|
|
86
|
+
f"got {type(val).__name__}"
|
|
87
|
+
)
|
|
88
|
+
return RuleClause(
|
|
89
|
+
relations=self._relations,
|
|
90
|
+
select_map=select_map,
|
|
91
|
+
condition=self._condition,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class From(_FromBase):
|
|
96
|
+
"""Rule builder: From(Relation1, Relation2, ...).where(...).select(...)"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, *relations: type[Relation] | RelationRef) -> None:
|
|
99
|
+
self._relations = []
|
|
100
|
+
for r in relations:
|
|
101
|
+
if isinstance(r, RelationRef):
|
|
102
|
+
self._relations.append((r.relation_name, r.relation_cls, r.alias))
|
|
103
|
+
elif isinstance(r, type) and issubclass(r, Relation):
|
|
104
|
+
rn = Relation._resolve_name(r)
|
|
105
|
+
self._relations.append((rn, r, None))
|
|
106
|
+
else:
|
|
107
|
+
raise TypeError(
|
|
108
|
+
f"From() expects Relation subclasses or RelationRef, "
|
|
109
|
+
f"got {type(r).__name__}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def where(self, condition: Any) -> FromWhere:
|
|
113
|
+
"""Add a filter condition. Accepts a BoolExpr or a lambda taking proxies."""
|
|
114
|
+
if callable(condition) and not isinstance(condition, BoolExpr):
|
|
115
|
+
proxies = self._build_proxy_args()
|
|
116
|
+
condition = condition(*proxies)
|
|
117
|
+
if not isinstance(condition, BoolExpr):
|
|
118
|
+
raise TypeError(
|
|
119
|
+
f"where() condition must be a BoolExpr or callable returning BoolExpr, "
|
|
120
|
+
f"got {type(condition).__name__}"
|
|
121
|
+
)
|
|
122
|
+
return FromWhere(self._relations, condition)
|
|
123
|
+
|
|
124
|
+
def select(self, **columns: ColumnProxy | Expr) -> RuleClause:
|
|
125
|
+
"""Map derived columns to body expressions (no filter)."""
|
|
126
|
+
select_map: dict[str, Expr] = {}
|
|
127
|
+
for name, val in columns.items():
|
|
128
|
+
if isinstance(val, ColumnProxy):
|
|
129
|
+
select_map[name] = val._to_ast()
|
|
130
|
+
elif isinstance(val, Expr):
|
|
131
|
+
select_map[name] = val
|
|
132
|
+
else:
|
|
133
|
+
raise TypeError(
|
|
134
|
+
f"select() value for '{name}' must be a Column or Expr, "
|
|
135
|
+
f"got {type(val).__name__}"
|
|
136
|
+
)
|
|
137
|
+
return RuleClause(
|
|
138
|
+
relations=self._relations,
|
|
139
|
+
select_map=select_map,
|
|
140
|
+
condition=None,
|
|
141
|
+
)
|
inputlayer/exceptions.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Exception hierarchy for the InputLayer OLM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InputLayerError(Exception):
|
|
7
|
+
"""Base exception for all InputLayer errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionError(InputLayerError):
|
|
11
|
+
"""Failed to connect or lost connection to the server."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(InputLayerError):
|
|
15
|
+
"""Authentication failed (bad credentials or API key)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SchemaConflictError(InputLayerError):
|
|
19
|
+
"""Schema definition conflicts with an existing schema."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
*,
|
|
25
|
+
existing_schema: dict | None = None,
|
|
26
|
+
proposed_schema: dict | None = None,
|
|
27
|
+
conflicts: list[str] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.existing_schema = existing_schema
|
|
31
|
+
self.proposed_schema = proposed_schema
|
|
32
|
+
self.conflicts = conflicts or []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ValidationError(InputLayerError):
|
|
36
|
+
"""Data validation failed (type mismatch, constraint violation)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, *, details: list[dict] | None = None) -> None:
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.details = details or []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class QueryTimeoutError(InputLayerError):
|
|
44
|
+
"""Query exceeded the configured timeout."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PermissionError(InputLayerError):
|
|
48
|
+
"""Insufficient permissions for the requested operation."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class KnowledgeGraphNotFoundError(InputLayerError):
|
|
52
|
+
"""The specified knowledge graph does not exist."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class KnowledgeGraphExistsError(InputLayerError):
|
|
56
|
+
"""The knowledge graph already exists."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CannotDropError(InputLayerError):
|
|
60
|
+
"""Cannot drop the target (e.g., default KG, currently bound KG)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RelationNotFoundError(InputLayerError):
|
|
64
|
+
"""The specified relation does not exist."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RuleNotFoundError(InputLayerError):
|
|
68
|
+
"""The specified rule does not exist."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class IndexNotFoundError(InputLayerError):
|
|
72
|
+
"""The specified index does not exist."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class InternalError(InputLayerError):
|
|
76
|
+
"""An unexpected internal error occurred."""
|