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/_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