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/_proxy.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Column proxy objects for building expression ASTs via operator overloading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from inputlayer._ast import (
|
|
8
|
+
AggExpr,
|
|
9
|
+
And,
|
|
10
|
+
Arithmetic,
|
|
11
|
+
BoolExpr,
|
|
12
|
+
Column as AstColumn,
|
|
13
|
+
Comparison,
|
|
14
|
+
Expr,
|
|
15
|
+
InExpr,
|
|
16
|
+
Literal,
|
|
17
|
+
MatchExpr,
|
|
18
|
+
NegatedIn,
|
|
19
|
+
Not,
|
|
20
|
+
Or,
|
|
21
|
+
OrderedColumn,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from inputlayer.relation import Relation
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ColumnProxy:
|
|
29
|
+
"""Proxy returned by Relation.column_name - builds AST nodes via operators."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, relation: str, name: str, *, ref_alias: str | None = None) -> None:
|
|
32
|
+
self._relation = relation
|
|
33
|
+
self._name = name
|
|
34
|
+
self._ref_alias = ref_alias
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def relation(self) -> str:
|
|
38
|
+
return self._relation
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def name(self) -> str:
|
|
42
|
+
return self._name
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def ref_alias(self) -> str | None:
|
|
46
|
+
return self._ref_alias
|
|
47
|
+
|
|
48
|
+
def _to_ast(self) -> AstColumn:
|
|
49
|
+
return AstColumn(self._relation, self._name, self._ref_alias)
|
|
50
|
+
|
|
51
|
+
# ── Comparison operators → BoolExpr ───────────────────────────────
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other: Any) -> Comparison: # type: ignore[override]
|
|
54
|
+
return Comparison("=", self._to_ast(), _wrap(other))
|
|
55
|
+
|
|
56
|
+
def __ne__(self, other: Any) -> Comparison: # type: ignore[override]
|
|
57
|
+
return Comparison("!=", self._to_ast(), _wrap(other))
|
|
58
|
+
|
|
59
|
+
def __lt__(self, other: Any) -> Comparison:
|
|
60
|
+
return Comparison("<", self._to_ast(), _wrap(other))
|
|
61
|
+
|
|
62
|
+
def __le__(self, other: Any) -> Comparison:
|
|
63
|
+
return Comparison("<=", self._to_ast(), _wrap(other))
|
|
64
|
+
|
|
65
|
+
def __gt__(self, other: Any) -> Comparison:
|
|
66
|
+
return Comparison(">", self._to_ast(), _wrap(other))
|
|
67
|
+
|
|
68
|
+
def __ge__(self, other: Any) -> Comparison:
|
|
69
|
+
return Comparison(">=", self._to_ast(), _wrap(other))
|
|
70
|
+
|
|
71
|
+
# ── Arithmetic operators → Expr ───────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def __add__(self, other: Any) -> Arithmetic:
|
|
74
|
+
return Arithmetic("+", self._to_ast(), _wrap(other))
|
|
75
|
+
|
|
76
|
+
def __radd__(self, other: Any) -> Arithmetic:
|
|
77
|
+
return Arithmetic("+", _wrap(other), self._to_ast())
|
|
78
|
+
|
|
79
|
+
def __sub__(self, other: Any) -> Arithmetic:
|
|
80
|
+
return Arithmetic("-", self._to_ast(), _wrap(other))
|
|
81
|
+
|
|
82
|
+
def __rsub__(self, other: Any) -> Arithmetic:
|
|
83
|
+
return Arithmetic("-", _wrap(other), self._to_ast())
|
|
84
|
+
|
|
85
|
+
def __mul__(self, other: Any) -> Arithmetic:
|
|
86
|
+
return Arithmetic("*", self._to_ast(), _wrap(other))
|
|
87
|
+
|
|
88
|
+
def __rmul__(self, other: Any) -> Arithmetic:
|
|
89
|
+
return Arithmetic("*", _wrap(other), self._to_ast())
|
|
90
|
+
|
|
91
|
+
def __truediv__(self, other: Any) -> Arithmetic:
|
|
92
|
+
return Arithmetic("/", self._to_ast(), _wrap(other))
|
|
93
|
+
|
|
94
|
+
def __rtruediv__(self, other: Any) -> Arithmetic:
|
|
95
|
+
return Arithmetic("/", _wrap(other), self._to_ast())
|
|
96
|
+
|
|
97
|
+
def __mod__(self, other: Any) -> Arithmetic:
|
|
98
|
+
return Arithmetic("%", self._to_ast(), _wrap(other))
|
|
99
|
+
|
|
100
|
+
def __rmod__(self, other: Any) -> Arithmetic:
|
|
101
|
+
return Arithmetic("%", _wrap(other), self._to_ast())
|
|
102
|
+
|
|
103
|
+
# ── Negation (bitwise NOT used as logical NOT) ────────────────────
|
|
104
|
+
|
|
105
|
+
def __invert__(self) -> ColumnProxy:
|
|
106
|
+
"""Returns a negated proxy (for ~Relation.col.in_(...) patterns)."""
|
|
107
|
+
return _NegatedColumnProxy(self)
|
|
108
|
+
|
|
109
|
+
# ── Membership ────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def in_(self, other: ColumnProxy) -> InExpr:
|
|
112
|
+
"""Test if this column's value appears in another relation's column."""
|
|
113
|
+
return InExpr(self._to_ast(), other._to_ast())
|
|
114
|
+
|
|
115
|
+
# ── Ordering ──────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def asc(self) -> OrderedColumn:
|
|
118
|
+
return OrderedColumn(self._to_ast(), descending=False)
|
|
119
|
+
|
|
120
|
+
def desc(self) -> OrderedColumn:
|
|
121
|
+
return OrderedColumn(self._to_ast(), descending=True)
|
|
122
|
+
|
|
123
|
+
# ── Multi-column match ────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def matches(
|
|
126
|
+
self, relation: type[Relation], on: dict[str, str]
|
|
127
|
+
) -> MatchExpr:
|
|
128
|
+
"""Check if columns match entries in another relation."""
|
|
129
|
+
from inputlayer.relation import Relation as RelBase
|
|
130
|
+
|
|
131
|
+
rel_name = RelBase._resolve_name(relation)
|
|
132
|
+
bindings = {}
|
|
133
|
+
for target_col, source_col_name in on.items():
|
|
134
|
+
# source_col_name refers to a column on self's relation
|
|
135
|
+
bindings[target_col] = AstColumn(self._relation, source_col_name, self._ref_alias)
|
|
136
|
+
return MatchExpr(rel_name, bindings, negated=False)
|
|
137
|
+
|
|
138
|
+
def __repr__(self) -> str:
|
|
139
|
+
if self._ref_alias:
|
|
140
|
+
return f"ColumnProxy({self._ref_alias}.{self._name})"
|
|
141
|
+
return f"ColumnProxy({self._relation}.{self._name})"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _NegatedColumnProxy(ColumnProxy):
|
|
145
|
+
"""Wrapper returned by ~col to flip .in_() to NegatedIn."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, inner: ColumnProxy) -> None:
|
|
148
|
+
super().__init__(inner._relation, inner._name, ref_alias=inner._ref_alias)
|
|
149
|
+
self._inner = inner
|
|
150
|
+
|
|
151
|
+
def in_(self, other: ColumnProxy) -> NegatedIn: # type: ignore[override]
|
|
152
|
+
return NegatedIn(self._inner._to_ast(), other._to_ast())
|
|
153
|
+
|
|
154
|
+
def matches( # type: ignore[override]
|
|
155
|
+
self, relation: type[Relation], on: dict[str, str]
|
|
156
|
+
) -> MatchExpr:
|
|
157
|
+
from inputlayer.relation import Relation as RelBase
|
|
158
|
+
|
|
159
|
+
rel_name = RelBase._resolve_name(relation)
|
|
160
|
+
bindings = {}
|
|
161
|
+
for target_col, source_col_name in on.items():
|
|
162
|
+
bindings[target_col] = AstColumn(self._relation, source_col_name, self._ref_alias)
|
|
163
|
+
return MatchExpr(rel_name, bindings, negated=True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RelationProxy:
|
|
167
|
+
"""Proxy object passed to where/on lambdas. Attribute access returns ColumnProxy."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, relation_name: str, *, ref_alias: str | None = None) -> None:
|
|
170
|
+
self._relation_name = relation_name
|
|
171
|
+
self._ref_alias = ref_alias
|
|
172
|
+
|
|
173
|
+
def __getattr__(self, name: str) -> ColumnProxy:
|
|
174
|
+
if name.startswith("_"):
|
|
175
|
+
raise AttributeError(name)
|
|
176
|
+
return ColumnProxy(self._relation_name, name, ref_alias=self._ref_alias)
|
|
177
|
+
|
|
178
|
+
def __repr__(self) -> str:
|
|
179
|
+
if self._ref_alias:
|
|
180
|
+
return f"RelationProxy({self._ref_alias})"
|
|
181
|
+
return f"RelationProxy({self._relation_name})"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RelationRef:
|
|
185
|
+
"""Independent reference to a relation for self-joins. Created by Relation.refs(n)."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, relation_cls: type[Relation], alias: str) -> None:
|
|
188
|
+
self._relation_cls = relation_cls
|
|
189
|
+
self._alias = alias
|
|
190
|
+
from inputlayer.relation import Relation as RelBase
|
|
191
|
+
|
|
192
|
+
self._relation_name = RelBase._resolve_name(relation_cls)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def alias(self) -> str:
|
|
196
|
+
return self._alias
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def relation_name(self) -> str:
|
|
200
|
+
return self._relation_name
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def relation_cls(self) -> type[Relation]:
|
|
204
|
+
return self._relation_cls
|
|
205
|
+
|
|
206
|
+
def __getattr__(self, name: str) -> ColumnProxy:
|
|
207
|
+
if name.startswith("_"):
|
|
208
|
+
raise AttributeError(name)
|
|
209
|
+
return ColumnProxy(self._relation_name, name, ref_alias=self._alias)
|
|
210
|
+
|
|
211
|
+
def __repr__(self) -> str:
|
|
212
|
+
return f"RelationRef({self._relation_name} as {self._alias})"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── BoolExpr combinator operators ─────────────────────────────────────
|
|
216
|
+
# Monkey-patch & | ~ on BoolExpr subclasses so cond1 & cond2 works.
|
|
217
|
+
|
|
218
|
+
def _bool_and(self: BoolExpr, other: BoolExpr) -> And:
|
|
219
|
+
return And(self, other)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _bool_or(self: BoolExpr, other: BoolExpr) -> Or:
|
|
223
|
+
return Or(self, other)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _bool_not(self: BoolExpr) -> Not:
|
|
227
|
+
return Not(self)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
for _cls in (Comparison, And, Or, Not, InExpr, NegatedIn, MatchExpr):
|
|
231
|
+
_cls.__and__ = _bool_and # type: ignore[attr-defined]
|
|
232
|
+
_cls.__or__ = _bool_or # type: ignore[attr-defined]
|
|
233
|
+
_cls.__invert__ = _bool_not # type: ignore[attr-defined]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ── Helpers ───────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
def _wrap(value: Any) -> Expr:
|
|
239
|
+
"""Wrap a raw Python value or proxy into an AST Expr."""
|
|
240
|
+
if isinstance(value, ColumnProxy):
|
|
241
|
+
return value._to_ast()
|
|
242
|
+
if isinstance(value, Expr):
|
|
243
|
+
return value
|
|
244
|
+
return Literal(value)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Aggregation functions that compile to Datalog aggregates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from inputlayer._ast import AggExpr, Expr
|
|
8
|
+
from inputlayer._proxy import ColumnProxy
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from inputlayer.relation import Relation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_expr(col: ColumnProxy | Expr) -> Expr:
|
|
15
|
+
if isinstance(col, ColumnProxy):
|
|
16
|
+
return col._to_ast()
|
|
17
|
+
return col
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def count(column: ColumnProxy | type[Relation] | None = None) -> AggExpr:
|
|
21
|
+
"""Count rows. If a column is given, counts non-null values.
|
|
22
|
+
|
|
23
|
+
Datalog: count<Var>
|
|
24
|
+
"""
|
|
25
|
+
if column is None or (isinstance(column, type)):
|
|
26
|
+
# count(*) - needs at least one column from the body
|
|
27
|
+
return AggExpr(func="count", column=None)
|
|
28
|
+
return AggExpr(func="count", column=_to_expr(column))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def count_distinct(column: ColumnProxy) -> AggExpr:
|
|
32
|
+
"""Count distinct values.
|
|
33
|
+
|
|
34
|
+
Datalog: count_distinct<Var>
|
|
35
|
+
"""
|
|
36
|
+
return AggExpr(func="count_distinct", column=_to_expr(column))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sum_(column: ColumnProxy) -> AggExpr:
|
|
40
|
+
"""Sum numeric values.
|
|
41
|
+
|
|
42
|
+
Datalog: sum<Var>
|
|
43
|
+
"""
|
|
44
|
+
return AggExpr(func="sum", column=_to_expr(column))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def min_(column: ColumnProxy) -> AggExpr:
|
|
48
|
+
"""Minimum value.
|
|
49
|
+
|
|
50
|
+
Datalog: min<Var>
|
|
51
|
+
"""
|
|
52
|
+
return AggExpr(func="min", column=_to_expr(column))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def max_(column: ColumnProxy) -> AggExpr:
|
|
56
|
+
"""Maximum value.
|
|
57
|
+
|
|
58
|
+
Datalog: max<Var>
|
|
59
|
+
"""
|
|
60
|
+
return AggExpr(func="max", column=_to_expr(column))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def avg(column: ColumnProxy) -> AggExpr:
|
|
64
|
+
"""Average value.
|
|
65
|
+
|
|
66
|
+
Datalog: avg<Var>
|
|
67
|
+
"""
|
|
68
|
+
return AggExpr(func="avg", column=_to_expr(column))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def top_k(
|
|
72
|
+
k: int,
|
|
73
|
+
*passthrough: ColumnProxy,
|
|
74
|
+
order_by: ColumnProxy,
|
|
75
|
+
desc: bool = True,
|
|
76
|
+
) -> AggExpr:
|
|
77
|
+
"""Top-K aggregation with ordering.
|
|
78
|
+
|
|
79
|
+
Datalog: top_k<k, Passthrough..., OrderCol:desc>
|
|
80
|
+
"""
|
|
81
|
+
return AggExpr(
|
|
82
|
+
func="top_k",
|
|
83
|
+
params=(k,),
|
|
84
|
+
passthrough=tuple(_to_expr(p) for p in passthrough),
|
|
85
|
+
order_column=_to_expr(order_by),
|
|
86
|
+
desc=desc,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def top_k_threshold(
|
|
91
|
+
k: int,
|
|
92
|
+
threshold: float,
|
|
93
|
+
*passthrough: ColumnProxy,
|
|
94
|
+
order_by: ColumnProxy,
|
|
95
|
+
desc: bool = True,
|
|
96
|
+
) -> AggExpr:
|
|
97
|
+
"""Top-K with threshold aggregation.
|
|
98
|
+
|
|
99
|
+
Datalog: top_k_threshold<k, threshold, Passthrough..., OrderCol:desc>
|
|
100
|
+
"""
|
|
101
|
+
return AggExpr(
|
|
102
|
+
func="top_k_threshold",
|
|
103
|
+
params=(k, threshold),
|
|
104
|
+
passthrough=tuple(_to_expr(p) for p in passthrough),
|
|
105
|
+
order_column=_to_expr(order_by),
|
|
106
|
+
desc=desc,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def within_radius(
|
|
111
|
+
max_distance: float,
|
|
112
|
+
*passthrough: ColumnProxy,
|
|
113
|
+
distance: ColumnProxy | Expr,
|
|
114
|
+
asc: bool = True,
|
|
115
|
+
) -> AggExpr:
|
|
116
|
+
"""Within-radius aggregation.
|
|
117
|
+
|
|
118
|
+
Datalog: within_radius<r, Passthrough..., DistCol:asc>
|
|
119
|
+
"""
|
|
120
|
+
return AggExpr(
|
|
121
|
+
func="within_radius",
|
|
122
|
+
params=(max_distance,),
|
|
123
|
+
passthrough=tuple(_to_expr(p) for p in passthrough),
|
|
124
|
+
order_column=_to_expr(distance),
|
|
125
|
+
desc=not asc,
|
|
126
|
+
)
|
inputlayer/auth.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Authentication helpers - data classes and meta-command compilation for user/key/ACL management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class UserInfo:
|
|
10
|
+
username: str
|
|
11
|
+
role: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ApiKeyInfo:
|
|
16
|
+
label: str
|
|
17
|
+
created_at: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class AclEntry:
|
|
22
|
+
username: str
|
|
23
|
+
role: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Meta command compilation ──────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compile_create_user(username: str, password: str, role: str = "viewer") -> str:
|
|
30
|
+
return f".user create {username} {password} {role}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compile_drop_user(username: str) -> str:
|
|
34
|
+
return f".user drop {username}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compile_set_password(username: str, new_password: str) -> str:
|
|
38
|
+
return f".user password {username} {new_password}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compile_set_role(username: str, role: str) -> str:
|
|
42
|
+
return f".user role {username} {role}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compile_list_users() -> str:
|
|
46
|
+
return ".user list"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compile_create_api_key(label: str) -> str:
|
|
50
|
+
return f".apikey create {label}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def compile_list_api_keys() -> str:
|
|
54
|
+
return ".apikey list"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compile_revoke_api_key(label: str) -> str:
|
|
58
|
+
return f".apikey revoke {label}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compile_grant_access(kg: str, username: str, role: str) -> str:
|
|
62
|
+
return f".kg acl grant {kg} {username} {role}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compile_revoke_access(kg: str, username: str) -> str:
|
|
66
|
+
return f".kg acl revoke {kg} {username}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def compile_list_acl(kg: str) -> str:
|
|
70
|
+
return f".kg acl list {kg}"
|
inputlayer/client.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""InputLayer - top-level async client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, AsyncIterator, Callable
|
|
6
|
+
|
|
7
|
+
from inputlayer.auth import (
|
|
8
|
+
AclEntry,
|
|
9
|
+
ApiKeyInfo,
|
|
10
|
+
UserInfo,
|
|
11
|
+
compile_create_api_key,
|
|
12
|
+
compile_create_user,
|
|
13
|
+
compile_drop_user,
|
|
14
|
+
compile_list_api_keys,
|
|
15
|
+
compile_list_users,
|
|
16
|
+
compile_revoke_api_key,
|
|
17
|
+
compile_set_password,
|
|
18
|
+
compile_set_role,
|
|
19
|
+
)
|
|
20
|
+
from inputlayer.connection import Connection
|
|
21
|
+
from inputlayer.knowledge_graph import KnowledgeGraph
|
|
22
|
+
from inputlayer.notifications import NotificationDispatcher, NotificationEvent
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InputLayer:
|
|
26
|
+
"""Async client for InputLayer knowledge graph engine.
|
|
27
|
+
|
|
28
|
+
Usage::
|
|
29
|
+
|
|
30
|
+
async with InputLayer("ws://localhost:8080/ws", username="admin", password="admin") as il:
|
|
31
|
+
kg = il.knowledge_graph("default")
|
|
32
|
+
await kg.define(Employee)
|
|
33
|
+
await kg.insert(Employee(id=1, name="Alice", department="eng", salary=120000.0, active=True))
|
|
34
|
+
result = await kg.query(Employee)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
url: str,
|
|
40
|
+
*,
|
|
41
|
+
username: str | None = None,
|
|
42
|
+
password: str | None = None,
|
|
43
|
+
api_key: str | None = None,
|
|
44
|
+
auto_reconnect: bool = True,
|
|
45
|
+
reconnect_delay: float = 1.0,
|
|
46
|
+
max_reconnect_attempts: int = 10,
|
|
47
|
+
initial_kg: str | None = None,
|
|
48
|
+
last_seq: int | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._conn = Connection(
|
|
51
|
+
url,
|
|
52
|
+
username=username,
|
|
53
|
+
password=password,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
auto_reconnect=auto_reconnect,
|
|
56
|
+
reconnect_delay=reconnect_delay,
|
|
57
|
+
max_reconnect_attempts=max_reconnect_attempts,
|
|
58
|
+
initial_kg=initial_kg,
|
|
59
|
+
last_seq=last_seq,
|
|
60
|
+
)
|
|
61
|
+
self._kgs: dict[str, KnowledgeGraph] = {}
|
|
62
|
+
|
|
63
|
+
# ── Connection lifecycle ──────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async def connect(self) -> None:
|
|
66
|
+
"""Connect and authenticate."""
|
|
67
|
+
await self._conn.connect()
|
|
68
|
+
|
|
69
|
+
async def close(self) -> None:
|
|
70
|
+
"""Close the connection."""
|
|
71
|
+
await self._conn.close()
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self) -> InputLayer:
|
|
74
|
+
await self.connect()
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
78
|
+
await self.close()
|
|
79
|
+
|
|
80
|
+
# ── Properties ────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def connected(self) -> bool:
|
|
84
|
+
return self._conn.connected
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def session_id(self) -> str | None:
|
|
88
|
+
return self._conn.session_id
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def server_version(self) -> str | None:
|
|
92
|
+
return self._conn.server_version
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def role(self) -> str | None:
|
|
96
|
+
return self._conn.role
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def last_seq(self) -> int:
|
|
100
|
+
return self._conn.last_seq
|
|
101
|
+
|
|
102
|
+
# ── KG management ─────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def knowledge_graph(self, name: str, *, create: bool = True) -> KnowledgeGraph:
|
|
105
|
+
"""Get a KnowledgeGraph handle. Switches the session's active KG."""
|
|
106
|
+
if name not in self._kgs:
|
|
107
|
+
self._kgs[name] = KnowledgeGraph(name, self._conn)
|
|
108
|
+
return self._kgs[name]
|
|
109
|
+
|
|
110
|
+
async def list_knowledge_graphs(self) -> list[str]:
|
|
111
|
+
"""List all knowledge graphs."""
|
|
112
|
+
result = await self._conn.execute(".kg list")
|
|
113
|
+
return [row[0] for row in result.rows] if result.rows else []
|
|
114
|
+
|
|
115
|
+
async def drop_knowledge_graph(self, name: str) -> None:
|
|
116
|
+
"""Drop a knowledge graph."""
|
|
117
|
+
await self._conn.execute(f".kg drop {name}")
|
|
118
|
+
self._kgs.pop(name, None)
|
|
119
|
+
|
|
120
|
+
# ── User management ───────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async def create_user(self, username: str, password: str, role: str = "viewer") -> None:
|
|
123
|
+
await self._conn.execute(compile_create_user(username, password, role))
|
|
124
|
+
|
|
125
|
+
async def drop_user(self, username: str) -> None:
|
|
126
|
+
await self._conn.execute(compile_drop_user(username))
|
|
127
|
+
|
|
128
|
+
async def set_password(self, username: str, new_password: str) -> None:
|
|
129
|
+
await self._conn.execute(compile_set_password(username, new_password))
|
|
130
|
+
|
|
131
|
+
async def set_role(self, username: str, role: str) -> None:
|
|
132
|
+
await self._conn.execute(compile_set_role(username, role))
|
|
133
|
+
|
|
134
|
+
async def list_users(self) -> list[UserInfo]:
|
|
135
|
+
result = await self._conn.execute(compile_list_users())
|
|
136
|
+
return [
|
|
137
|
+
UserInfo(username=row[0], role=row[1])
|
|
138
|
+
for row in result.rows
|
|
139
|
+
if len(row) >= 2
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# ── API key management ────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
async def create_api_key(self, label: str) -> str:
|
|
145
|
+
"""Create an API key. Returns the key string."""
|
|
146
|
+
result = await self._conn.execute(compile_create_api_key(label))
|
|
147
|
+
if result.rows and result.rows[0]:
|
|
148
|
+
return str(result.rows[0][0])
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
async def list_api_keys(self) -> list[ApiKeyInfo]:
|
|
152
|
+
result = await self._conn.execute(compile_list_api_keys())
|
|
153
|
+
return [
|
|
154
|
+
ApiKeyInfo(label=row[0], created_at=str(row[1]) if len(row) > 1 else "")
|
|
155
|
+
for row in result.rows
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
async def revoke_api_key(self, label: str) -> None:
|
|
159
|
+
await self._conn.execute(compile_revoke_api_key(label))
|
|
160
|
+
|
|
161
|
+
# ── Notifications ─────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
def on(
|
|
164
|
+
self,
|
|
165
|
+
event_type: str,
|
|
166
|
+
*,
|
|
167
|
+
relation: str | None = None,
|
|
168
|
+
knowledge_graph: str | None = None,
|
|
169
|
+
) -> Callable:
|
|
170
|
+
"""Register a notification callback. Use as a decorator."""
|
|
171
|
+
return self._conn.dispatcher.on(
|
|
172
|
+
event_type, relation=relation, knowledge_graph=knowledge_graph
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def notifications(self) -> AsyncIterator[NotificationEvent]:
|
|
176
|
+
"""Async iterator yielding notification events."""
|
|
177
|
+
async for event in self._conn.dispatcher:
|
|
178
|
+
yield event
|