inputlayer-client-dev 0.1.0__py3-none-any.whl → 0.1.0.dev913__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 +87 -75
- inputlayer/_ast.py +4 -5
- inputlayer/_naming.py +2 -2
- inputlayer/_protocol.py +12 -3
- inputlayer/_proxy.py +22 -3
- inputlayer/_sync.py +104 -0
- inputlayer/aggregations.py +10 -10
- inputlayer/auth.py +48 -1
- inputlayer/client.py +43 -8
- inputlayer/client_sync.py +97 -68
- inputlayer/compiler.py +125 -104
- inputlayer/connection.py +74 -13
- inputlayer/derived.py +2 -2
- inputlayer/exceptions.py +24 -2
- inputlayer/functions.py +2 -2
- inputlayer/index.py +2 -2
- inputlayer/integrations/__init__.py +8 -0
- inputlayer/integrations/langchain/__init__.py +38 -0
- inputlayer/integrations/langchain/params.py +130 -0
- inputlayer/integrations/langchain/retriever.py +354 -0
- inputlayer/integrations/langchain/tool.py +460 -0
- inputlayer/integrations/langchain/vector_store.py +651 -0
- inputlayer/integrations/langgraph/__init__.py +60 -0
- inputlayer/integrations/langgraph/_checkpoint_serde.py +96 -0
- inputlayer/integrations/langgraph/_checkpointer_mixin.py +357 -0
- inputlayer/integrations/langgraph/_memory_helpers.py +181 -0
- inputlayer/integrations/langgraph/_memory_mixin.py +139 -0
- inputlayer/integrations/langgraph/_utils.py +145 -0
- inputlayer/integrations/langgraph/checkpointer.py +506 -0
- inputlayer/integrations/langgraph/memory.py +557 -0
- inputlayer/integrations/langgraph/nodes.py +190 -0
- inputlayer/integrations/langgraph/router.py +155 -0
- inputlayer/integrations/langgraph/state.py +40 -0
- inputlayer/knowledge_graph.py +507 -104
- inputlayer/migrations/__init__.py +11 -11
- inputlayer/migrations/cli.py +14 -5
- inputlayer/migrations/executor.py +2 -4
- inputlayer/migrations/loader.py +0 -1
- inputlayer/migrations/operations.py +12 -12
- inputlayer/migrations/recorder.py +3 -3
- inputlayer/migrations/state.py +6 -6
- inputlayer/migrations/writer.py +32 -7
- inputlayer/notifications.py +16 -5
- inputlayer/relation.py +3 -3
- inputlayer/result.py +20 -6
- inputlayer/session.py +5 -7
- inputlayer/types.py +4 -4
- {inputlayer_client_dev-0.1.0.dist-info → inputlayer_client_dev-0.1.0.dev913.dist-info}/METADATA +222 -11
- inputlayer_client_dev-0.1.0.dev913.dist-info/RECORD +53 -0
- inputlayer_client_dev-0.1.0.dist-info/RECORD +0 -35
- {inputlayer_client_dev-0.1.0.dist-info → inputlayer_client_dev-0.1.0.dev913.dist-info}/WHEEL +0 -0
- {inputlayer_client_dev-0.1.0.dist-info → inputlayer_client_dev-0.1.0.dev913.dist-info}/entry_points.txt +0 -0
inputlayer/__init__.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"""inputlayer - Python Object-Logic Mapper for InputLayer knowledge graph engine."""
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
from inputlayer
|
|
5
|
-
|
|
6
|
-
# Relation system
|
|
7
|
-
from inputlayer.relation import Relation
|
|
8
|
-
from inputlayer.derived import Derived, From, RuleClause
|
|
3
|
+
# Functions (re-export all)
|
|
4
|
+
from inputlayer import functions
|
|
9
5
|
|
|
10
6
|
# Aggregations
|
|
11
7
|
from inputlayer.aggregations import (
|
|
@@ -20,11 +16,13 @@ from inputlayer.aggregations import (
|
|
|
20
16
|
within_radius,
|
|
21
17
|
)
|
|
22
18
|
|
|
23
|
-
#
|
|
24
|
-
from inputlayer import
|
|
19
|
+
# Auth
|
|
20
|
+
from inputlayer.auth import AclEntry, ApiKeyInfo, UserInfo
|
|
25
21
|
|
|
26
|
-
#
|
|
27
|
-
from inputlayer.
|
|
22
|
+
# Client
|
|
23
|
+
from inputlayer.client import InputLayer
|
|
24
|
+
from inputlayer.client_sync import InputLayerSync
|
|
25
|
+
from inputlayer.derived import Derived, From, RuleClause
|
|
28
26
|
|
|
29
27
|
# Exceptions
|
|
30
28
|
from inputlayer.exceptions import (
|
|
@@ -32,11 +30,14 @@ from inputlayer.exceptions import (
|
|
|
32
30
|
CannotDropError,
|
|
33
31
|
ConnectionError,
|
|
34
32
|
IndexNotFoundError,
|
|
33
|
+
InputLayerConnectionError,
|
|
35
34
|
InputLayerError,
|
|
35
|
+
InputLayerPermissionError,
|
|
36
36
|
InternalError,
|
|
37
37
|
KnowledgeGraphExistsError,
|
|
38
38
|
KnowledgeGraphNotFoundError,
|
|
39
39
|
PermissionError,
|
|
40
|
+
QueryError,
|
|
40
41
|
QueryTimeoutError,
|
|
41
42
|
RelationNotFoundError,
|
|
42
43
|
RuleNotFoundError,
|
|
@@ -44,108 +45,119 @@ from inputlayer.exceptions import (
|
|
|
44
45
|
ValidationError,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
-
from inputlayer.
|
|
49
|
-
|
|
50
|
-
# Client
|
|
51
|
-
from inputlayer.client import InputLayer
|
|
52
|
-
from inputlayer.client_sync import InputLayerSync
|
|
48
|
+
# Index
|
|
49
|
+
from inputlayer.index import HnswIndex
|
|
53
50
|
|
|
54
51
|
# Knowledge Graph
|
|
55
52
|
from inputlayer.knowledge_graph import (
|
|
56
53
|
ClearResult,
|
|
57
54
|
ColumnInfo,
|
|
55
|
+
DebugResult,
|
|
58
56
|
DeleteResult,
|
|
59
|
-
ExplainResult,
|
|
60
57
|
IndexInfo,
|
|
61
58
|
IndexStats,
|
|
62
59
|
InsertResult,
|
|
63
60
|
KnowledgeGraph,
|
|
61
|
+
ProofNode,
|
|
62
|
+
ProofTree,
|
|
64
63
|
RelationDescription,
|
|
65
64
|
RelationInfo,
|
|
66
65
|
RuleInfo,
|
|
67
66
|
ServerStatus,
|
|
67
|
+
WhyNotResult,
|
|
68
|
+
WhyResult,
|
|
68
69
|
)
|
|
69
70
|
|
|
70
|
-
#
|
|
71
|
-
from inputlayer.
|
|
72
|
-
|
|
73
|
-
# Session
|
|
74
|
-
from inputlayer.session import Session
|
|
71
|
+
# Migrations
|
|
72
|
+
from inputlayer.migrations import Migration
|
|
75
73
|
|
|
76
74
|
# Notifications
|
|
77
75
|
from inputlayer.notifications import NotificationEvent
|
|
78
76
|
|
|
79
|
-
#
|
|
80
|
-
from inputlayer.
|
|
77
|
+
# Relation system
|
|
78
|
+
from inputlayer.relation import Relation
|
|
79
|
+
|
|
80
|
+
# Result
|
|
81
|
+
from inputlayer.result import ResultSet
|
|
82
|
+
|
|
83
|
+
# Session
|
|
84
|
+
from inputlayer.session import Session
|
|
85
|
+
from inputlayer.types import Timestamp, Vector, VectorInt8
|
|
81
86
|
|
|
82
87
|
__version__ = "0.1.0"
|
|
83
88
|
|
|
84
89
|
__all__ = [
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
"
|
|
90
|
+
"AclEntry",
|
|
91
|
+
"ApiKeyInfo",
|
|
92
|
+
"AuthenticationError",
|
|
93
|
+
"CannotDropError",
|
|
94
|
+
"ClearResult",
|
|
95
|
+
"ColumnInfo",
|
|
96
|
+
"ConnectionError",
|
|
97
|
+
"DebugResult",
|
|
98
|
+
"DeleteResult",
|
|
91
99
|
"Derived",
|
|
92
100
|
"From",
|
|
93
|
-
"RuleClause",
|
|
94
|
-
# Aggregations
|
|
95
|
-
"count",
|
|
96
|
-
"count_distinct",
|
|
97
|
-
"sum_",
|
|
98
|
-
"min_",
|
|
99
|
-
"max_",
|
|
100
|
-
"avg",
|
|
101
|
-
"top_k",
|
|
102
|
-
"top_k_threshold",
|
|
103
|
-
"within_radius",
|
|
104
|
-
# Functions
|
|
105
|
-
"functions",
|
|
106
101
|
# Index
|
|
107
102
|
"HnswIndex",
|
|
108
|
-
|
|
109
|
-
"InputLayerError",
|
|
110
|
-
"ConnectionError",
|
|
111
|
-
"AuthenticationError",
|
|
112
|
-
"SchemaConflictError",
|
|
113
|
-
"ValidationError",
|
|
114
|
-
"QueryTimeoutError",
|
|
115
|
-
"PermissionError",
|
|
116
|
-
"KnowledgeGraphNotFoundError",
|
|
117
|
-
"KnowledgeGraphExistsError",
|
|
118
|
-
"CannotDropError",
|
|
119
|
-
"RelationNotFoundError",
|
|
120
|
-
"RuleNotFoundError",
|
|
103
|
+
"IndexInfo",
|
|
121
104
|
"IndexNotFoundError",
|
|
122
|
-
"
|
|
123
|
-
# Result
|
|
124
|
-
"ResultSet",
|
|
105
|
+
"IndexStats",
|
|
125
106
|
# Client
|
|
126
107
|
"InputLayer",
|
|
108
|
+
# Exceptions
|
|
109
|
+
"InputLayerConnectionError",
|
|
110
|
+
"InputLayerError",
|
|
111
|
+
"InputLayerPermissionError",
|
|
127
112
|
"InputLayerSync",
|
|
113
|
+
"InsertResult",
|
|
114
|
+
"InternalError",
|
|
128
115
|
# KG
|
|
129
116
|
"KnowledgeGraph",
|
|
130
|
-
"
|
|
117
|
+
"KnowledgeGraphExistsError",
|
|
118
|
+
"KnowledgeGraphNotFoundError",
|
|
119
|
+
# Migrations
|
|
120
|
+
"Migration",
|
|
121
|
+
# Notifications
|
|
122
|
+
"NotificationEvent",
|
|
123
|
+
"PermissionError",
|
|
124
|
+
"ProofNode",
|
|
125
|
+
"ProofTree",
|
|
126
|
+
"QueryError",
|
|
127
|
+
"QueryTimeoutError",
|
|
128
|
+
# Relation
|
|
129
|
+
"Relation",
|
|
131
130
|
"RelationDescription",
|
|
132
|
-
"
|
|
131
|
+
"RelationInfo",
|
|
132
|
+
"RelationNotFoundError",
|
|
133
|
+
# Result
|
|
134
|
+
"ResultSet",
|
|
135
|
+
"RuleClause",
|
|
133
136
|
"RuleInfo",
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"InsertResult",
|
|
137
|
-
"DeleteResult",
|
|
138
|
-
"ClearResult",
|
|
139
|
-
"ExplainResult",
|
|
137
|
+
"RuleNotFoundError",
|
|
138
|
+
"SchemaConflictError",
|
|
140
139
|
"ServerStatus",
|
|
141
|
-
# Auth
|
|
142
|
-
"UserInfo",
|
|
143
|
-
"ApiKeyInfo",
|
|
144
|
-
"AclEntry",
|
|
145
140
|
# Session
|
|
146
141
|
"Session",
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
142
|
+
"Timestamp",
|
|
143
|
+
# Auth
|
|
144
|
+
"UserInfo",
|
|
145
|
+
"ValidationError",
|
|
146
|
+
# Types
|
|
147
|
+
"Vector",
|
|
148
|
+
"VectorInt8",
|
|
149
|
+
"WhyNotResult",
|
|
150
|
+
"WhyResult",
|
|
151
|
+
"avg",
|
|
152
|
+
# Aggregations
|
|
153
|
+
"count",
|
|
154
|
+
"count_distinct",
|
|
155
|
+
# Functions
|
|
156
|
+
"functions",
|
|
157
|
+
"max_",
|
|
158
|
+
"min_",
|
|
159
|
+
"sum_",
|
|
160
|
+
"top_k",
|
|
161
|
+
"top_k_threshold",
|
|
162
|
+
"within_radius",
|
|
151
163
|
]
|
inputlayer/_ast.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
"""Internal AST nodes for expression trees compiled to
|
|
1
|
+
"""Internal AST nodes for expression trees compiled to IQL."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
# ── Base ──────────────────────────────────────────────────────────────
|
|
10
9
|
|
|
11
10
|
@dataclass(frozen=True)
|
|
@@ -93,7 +92,7 @@ class Comparison(BoolExpr):
|
|
|
93
92
|
|
|
94
93
|
@dataclass(frozen=True)
|
|
95
94
|
class And(BoolExpr):
|
|
96
|
-
"""Logical AND of two conditions (
|
|
95
|
+
"""Logical AND of two conditions (IQL comma)."""
|
|
97
96
|
left: BoolExpr
|
|
98
97
|
right: BoolExpr
|
|
99
98
|
|
|
@@ -107,7 +106,7 @@ class Or(BoolExpr):
|
|
|
107
106
|
|
|
108
107
|
@dataclass(frozen=True)
|
|
109
108
|
class Not(BoolExpr):
|
|
110
|
-
"""Negation: !relation(X, Y) in
|
|
109
|
+
"""Negation: !relation(X, Y) in IQL."""
|
|
111
110
|
operand: BoolExpr
|
|
112
111
|
|
|
113
112
|
|
inputlayer/_naming.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Naming convention utilities: CamelCase ↔ snake_case, column →
|
|
1
|
+
"""Naming convention utilities: CamelCase ↔ snake_case, column → IQL variable."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -33,7 +33,7 @@ def snake_to_camel(name: str) -> str:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def column_to_variable(column_name: str) -> str:
|
|
36
|
-
"""Convert a snake_case column name to
|
|
36
|
+
"""Convert a snake_case column name to an IQL variable (Capitalized).
|
|
37
37
|
|
|
38
38
|
Examples:
|
|
39
39
|
id -> Id
|
inputlayer/_protocol.py
CHANGED
|
@@ -6,10 +6,9 @@ Matches the AsyncAPI spec at ``docs/spec/asyncapi.yaml``.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import dataclass
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
# ── Client → Server messages ──────────────────────────────────────────
|
|
14
13
|
|
|
15
14
|
@dataclass(frozen=True)
|
|
@@ -79,6 +78,8 @@ class ResultResponse:
|
|
|
79
78
|
row_provenance: list[str] | None = None
|
|
80
79
|
metadata: dict[str, Any] | None = None
|
|
81
80
|
switched_kg: str | None = None
|
|
81
|
+
proof_trees: list[dict[str, Any]] | None = None
|
|
82
|
+
timing_breakdown: dict[str, Any] | None = None
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
@dataclass(frozen=True)
|
|
@@ -95,6 +96,8 @@ class ResultStartResponse:
|
|
|
95
96
|
execution_time_ms: int
|
|
96
97
|
metadata: dict[str, Any] | None = None
|
|
97
98
|
switched_kg: str | None = None
|
|
99
|
+
proof_trees: list[dict[str, Any]] | None = None
|
|
100
|
+
timing_breakdown: dict[str, Any] | None = None
|
|
98
101
|
|
|
99
102
|
|
|
100
103
|
@dataclass(frozen=True)
|
|
@@ -149,7 +152,9 @@ ServerMessage = (
|
|
|
149
152
|
|
|
150
153
|
# ── Serialization / Deserialization ───────────────────────────────────
|
|
151
154
|
|
|
152
|
-
def serialize_message(
|
|
155
|
+
def serialize_message(
|
|
156
|
+
msg: LoginMessage | AuthenticateMessage | ExecuteMessage | PingMessage,
|
|
157
|
+
) -> str:
|
|
153
158
|
"""Serialize a client message to JSON."""
|
|
154
159
|
return msg.to_json()
|
|
155
160
|
|
|
@@ -181,6 +186,8 @@ def deserialize_message(data: str | bytes) -> ServerMessage:
|
|
|
181
186
|
row_provenance=obj.get("row_provenance"),
|
|
182
187
|
metadata=obj.get("metadata"),
|
|
183
188
|
switched_kg=obj.get("switched_kg"),
|
|
189
|
+
proof_trees=obj.get("proof_trees"),
|
|
190
|
+
timing_breakdown=obj.get("timing_breakdown"),
|
|
184
191
|
)
|
|
185
192
|
if msg_type == "error":
|
|
186
193
|
return ErrorResponse(
|
|
@@ -195,6 +202,8 @@ def deserialize_message(data: str | bytes) -> ServerMessage:
|
|
|
195
202
|
execution_time_ms=obj["execution_time_ms"],
|
|
196
203
|
metadata=obj.get("metadata"),
|
|
197
204
|
switched_kg=obj.get("switched_kg"),
|
|
205
|
+
proof_trees=obj.get("proof_trees"),
|
|
206
|
+
timing_breakdown=obj.get("timing_breakdown"),
|
|
198
207
|
)
|
|
199
208
|
if msg_type == "result_chunk":
|
|
200
209
|
return ResultChunkResponse(
|
inputlayer/_proxy.py
CHANGED
|
@@ -5,11 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from inputlayer._ast import (
|
|
8
|
-
AggExpr,
|
|
9
8
|
And,
|
|
10
9
|
Arithmetic,
|
|
11
10
|
BoolExpr,
|
|
12
|
-
Column as AstColumn,
|
|
13
11
|
Comparison,
|
|
14
12
|
Expr,
|
|
15
13
|
InExpr,
|
|
@@ -20,6 +18,9 @@ from inputlayer._ast import (
|
|
|
20
18
|
Or,
|
|
21
19
|
OrderedColumn,
|
|
22
20
|
)
|
|
21
|
+
from inputlayer._ast import (
|
|
22
|
+
Column as AstColumn,
|
|
23
|
+
)
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from inputlayer.relation import Relation
|
|
@@ -28,10 +29,28 @@ if TYPE_CHECKING:
|
|
|
28
29
|
class ColumnProxy:
|
|
29
30
|
"""Proxy returned by Relation.column_name - builds AST nodes via operators."""
|
|
30
31
|
|
|
31
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
relation: str,
|
|
35
|
+
name: str,
|
|
36
|
+
*,
|
|
37
|
+
ref_alias: str | None = None,
|
|
38
|
+
relation_cls: type | None = None,
|
|
39
|
+
) -> None:
|
|
32
40
|
self._relation = relation
|
|
33
41
|
self._name = name
|
|
34
42
|
self._ref_alias = ref_alias
|
|
43
|
+
self._relation_cls = relation_cls
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def relation_cls(self) -> type | None:
|
|
47
|
+
"""The originating ``Relation`` subclass, if known.
|
|
48
|
+
|
|
49
|
+
Set when the proxy was created via attribute access on a
|
|
50
|
+
``Relation`` class (``Sale.region``). Stays ``None`` for
|
|
51
|
+
proxies built from raw column references.
|
|
52
|
+
"""
|
|
53
|
+
return self._relation_cls
|
|
35
54
|
|
|
36
55
|
@property
|
|
37
56
|
def relation(self) -> str:
|
inputlayer/_sync.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Sync-from-async bridge using a dedicated background event loop thread.
|
|
2
|
+
|
|
3
|
+
Safe to call from any context: plain scripts, Jupyter notebooks,
|
|
4
|
+
inside running event loops (FastAPI, LangGraph), etc.
|
|
5
|
+
|
|
6
|
+
This is the same pattern used by httpx.Client and playwright.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import atexit
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Coroutine
|
|
16
|
+
from typing import Any, TypeVar
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Default timeout (seconds) for waiting on coroutine results.
|
|
23
|
+
# None means wait indefinitely (matching httpx behavior).
|
|
24
|
+
DEFAULT_TIMEOUT: float | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _LoopThread:
|
|
28
|
+
"""Background thread owning its own event loop."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, timeout: float | None = DEFAULT_TIMEOUT) -> None:
|
|
31
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
32
|
+
self._thread: threading.Thread | None = None
|
|
33
|
+
self._lock = threading.Lock()
|
|
34
|
+
self._timeout = timeout
|
|
35
|
+
|
|
36
|
+
def _ensure_running(self) -> asyncio.AbstractEventLoop:
|
|
37
|
+
# Fast path: already running (no lock needed for a quick read -
|
|
38
|
+
# the lock below handles the actual creation race).
|
|
39
|
+
loop = self._loop
|
|
40
|
+
if loop is not None and loop.is_running():
|
|
41
|
+
return loop
|
|
42
|
+
with self._lock:
|
|
43
|
+
# Re-check under lock to avoid creating a duplicate.
|
|
44
|
+
if self._loop is not None and self._loop.is_running():
|
|
45
|
+
return self._loop
|
|
46
|
+
new_loop = asyncio.new_event_loop()
|
|
47
|
+
thread = threading.Thread(
|
|
48
|
+
target=new_loop.run_forever,
|
|
49
|
+
daemon=True,
|
|
50
|
+
name="inputlayer-sync",
|
|
51
|
+
)
|
|
52
|
+
thread.start()
|
|
53
|
+
self._loop = new_loop
|
|
54
|
+
self._thread = thread
|
|
55
|
+
return new_loop
|
|
56
|
+
|
|
57
|
+
def run(self, coro: Coroutine[Any, Any, T]) -> T:
|
|
58
|
+
"""Submit a coroutine to the background loop and block until done."""
|
|
59
|
+
if threading.current_thread() is self._thread:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
"Cannot call run_sync from the background event loop thread "
|
|
62
|
+
"- use 'await' instead. This would deadlock."
|
|
63
|
+
)
|
|
64
|
+
loop = self._ensure_running()
|
|
65
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
66
|
+
return future.result(timeout=self._timeout)
|
|
67
|
+
|
|
68
|
+
def shutdown(self) -> None:
|
|
69
|
+
"""Stop the background loop and join the thread."""
|
|
70
|
+
with self._lock:
|
|
71
|
+
loop = self._loop
|
|
72
|
+
thread = self._thread
|
|
73
|
+
self._loop = None
|
|
74
|
+
self._thread = None
|
|
75
|
+
|
|
76
|
+
if loop is not None and loop.is_running():
|
|
77
|
+
try:
|
|
78
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
79
|
+
except RuntimeError:
|
|
80
|
+
# Loop already closed or stopping - not an error.
|
|
81
|
+
logger.debug("Loop already stopped during shutdown")
|
|
82
|
+
if thread is not None:
|
|
83
|
+
thread.join(timeout=5)
|
|
84
|
+
if thread.is_alive():
|
|
85
|
+
logger.warning(
|
|
86
|
+
"inputlayer-sync thread did not stop within 5 s; "
|
|
87
|
+
"it will be cleaned up at process exit (daemon thread)."
|
|
88
|
+
)
|
|
89
|
+
if loop is not None and not loop.is_closed():
|
|
90
|
+
loop.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_default_thread = _LoopThread()
|
|
94
|
+
atexit.register(_default_thread.shutdown)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_sync(coro: Coroutine[Any, Any, T]) -> T:
|
|
98
|
+
"""Run an async coroutine from synchronous code. Always safe.
|
|
99
|
+
|
|
100
|
+
Uses a module-level background thread with its own event loop,
|
|
101
|
+
so it works even when the caller already has a running loop
|
|
102
|
+
(Jupyter, FastAPI, LangGraph, etc.).
|
|
103
|
+
"""
|
|
104
|
+
return _default_thread.run(coro)
|
inputlayer/aggregations.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Aggregation functions that compile to
|
|
1
|
+
"""Aggregation functions that compile to IQL aggregates."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ def _to_expr(col: ColumnProxy | Expr) -> Expr:
|
|
|
20
20
|
def count(column: ColumnProxy | type[Relation] | None = None) -> AggExpr:
|
|
21
21
|
"""Count rows. If a column is given, counts non-null values.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
IQL: count<Var>
|
|
24
24
|
"""
|
|
25
25
|
if column is None or (isinstance(column, type)):
|
|
26
26
|
# count(*) - needs at least one column from the body
|
|
@@ -31,7 +31,7 @@ def count(column: ColumnProxy | type[Relation] | None = None) -> AggExpr:
|
|
|
31
31
|
def count_distinct(column: ColumnProxy) -> AggExpr:
|
|
32
32
|
"""Count distinct values.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
IQL: count_distinct<Var>
|
|
35
35
|
"""
|
|
36
36
|
return AggExpr(func="count_distinct", column=_to_expr(column))
|
|
37
37
|
|
|
@@ -39,7 +39,7 @@ def count_distinct(column: ColumnProxy) -> AggExpr:
|
|
|
39
39
|
def sum_(column: ColumnProxy) -> AggExpr:
|
|
40
40
|
"""Sum numeric values.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
IQL: sum<Var>
|
|
43
43
|
"""
|
|
44
44
|
return AggExpr(func="sum", column=_to_expr(column))
|
|
45
45
|
|
|
@@ -47,7 +47,7 @@ def sum_(column: ColumnProxy) -> AggExpr:
|
|
|
47
47
|
def min_(column: ColumnProxy) -> AggExpr:
|
|
48
48
|
"""Minimum value.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
IQL: min<Var>
|
|
51
51
|
"""
|
|
52
52
|
return AggExpr(func="min", column=_to_expr(column))
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ def min_(column: ColumnProxy) -> AggExpr:
|
|
|
55
55
|
def max_(column: ColumnProxy) -> AggExpr:
|
|
56
56
|
"""Maximum value.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
IQL: max<Var>
|
|
59
59
|
"""
|
|
60
60
|
return AggExpr(func="max", column=_to_expr(column))
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ def max_(column: ColumnProxy) -> AggExpr:
|
|
|
63
63
|
def avg(column: ColumnProxy) -> AggExpr:
|
|
64
64
|
"""Average value.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
IQL: avg<Var>
|
|
67
67
|
"""
|
|
68
68
|
return AggExpr(func="avg", column=_to_expr(column))
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ def top_k(
|
|
|
76
76
|
) -> AggExpr:
|
|
77
77
|
"""Top-K aggregation with ordering.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
IQL: top_k<k, Passthrough..., OrderCol:desc>
|
|
80
80
|
"""
|
|
81
81
|
return AggExpr(
|
|
82
82
|
func="top_k",
|
|
@@ -96,7 +96,7 @@ def top_k_threshold(
|
|
|
96
96
|
) -> AggExpr:
|
|
97
97
|
"""Top-K with threshold aggregation.
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
IQL: top_k_threshold<k, threshold, Passthrough..., OrderCol:desc>
|
|
100
100
|
"""
|
|
101
101
|
return AggExpr(
|
|
102
102
|
func="top_k_threshold",
|
|
@@ -115,7 +115,7 @@ def within_radius(
|
|
|
115
115
|
) -> AggExpr:
|
|
116
116
|
"""Within-radius aggregation.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
IQL: within_radius<r, Passthrough..., DistCol:asc>
|
|
119
119
|
"""
|
|
120
120
|
return AggExpr(
|
|
121
121
|
func="within_radius",
|
inputlayer/auth.py
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
|
-
"""Authentication helpers
|
|
1
|
+
"""Authentication helpers.
|
|
2
|
+
|
|
3
|
+
Data classes and meta-command compilation for user/key/ACL management.
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from __future__ import annotations
|
|
4
7
|
|
|
8
|
+
import re
|
|
5
9
|
from dataclasses import dataclass
|
|
6
10
|
|
|
11
|
+
_SAFE_IDENTIFIER = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
12
|
+
# Passwords allow more characters but no whitespace or control chars
|
|
13
|
+
_SAFE_PASSWORD = re.compile(r"^\S+$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_identifier(value: str, name: str) -> str:
|
|
17
|
+
"""Validate that a value is safe for use in meta-commands."""
|
|
18
|
+
if not value:
|
|
19
|
+
raise ValueError(f"{name} must not be empty")
|
|
20
|
+
if not _SAFE_IDENTIFIER.match(value):
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"{name} contains invalid characters: {value!r}. "
|
|
23
|
+
f"Only letters, digits, underscores, dots, and hyphens are allowed."
|
|
24
|
+
)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_password(value: str, name: str) -> str:
|
|
29
|
+
"""Validate a password for use in space-delimited meta-commands."""
|
|
30
|
+
if not value:
|
|
31
|
+
raise ValueError(f"{name} must not be empty")
|
|
32
|
+
if not _SAFE_PASSWORD.match(value):
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"{name} must not contain whitespace: {value!r}"
|
|
35
|
+
)
|
|
36
|
+
return value
|
|
37
|
+
|
|
7
38
|
|
|
8
39
|
@dataclass(frozen=True)
|
|
9
40
|
class UserInfo:
|
|
@@ -27,18 +58,26 @@ class AclEntry:
|
|
|
27
58
|
|
|
28
59
|
|
|
29
60
|
def compile_create_user(username: str, password: str, role: str = "viewer") -> str:
|
|
61
|
+
_validate_identifier(username, "username")
|
|
62
|
+
_validate_password(password, "password")
|
|
63
|
+
_validate_identifier(role, "role")
|
|
30
64
|
return f".user create {username} {password} {role}"
|
|
31
65
|
|
|
32
66
|
|
|
33
67
|
def compile_drop_user(username: str) -> str:
|
|
68
|
+
_validate_identifier(username, "username")
|
|
34
69
|
return f".user drop {username}"
|
|
35
70
|
|
|
36
71
|
|
|
37
72
|
def compile_set_password(username: str, new_password: str) -> str:
|
|
73
|
+
_validate_identifier(username, "username")
|
|
74
|
+
_validate_password(new_password, "new_password")
|
|
38
75
|
return f".user password {username} {new_password}"
|
|
39
76
|
|
|
40
77
|
|
|
41
78
|
def compile_set_role(username: str, role: str) -> str:
|
|
79
|
+
_validate_identifier(username, "username")
|
|
80
|
+
_validate_identifier(role, "role")
|
|
42
81
|
return f".user role {username} {role}"
|
|
43
82
|
|
|
44
83
|
|
|
@@ -47,6 +86,7 @@ def compile_list_users() -> str:
|
|
|
47
86
|
|
|
48
87
|
|
|
49
88
|
def compile_create_api_key(label: str) -> str:
|
|
89
|
+
_validate_identifier(label, "label")
|
|
50
90
|
return f".apikey create {label}"
|
|
51
91
|
|
|
52
92
|
|
|
@@ -55,16 +95,23 @@ def compile_list_api_keys() -> str:
|
|
|
55
95
|
|
|
56
96
|
|
|
57
97
|
def compile_revoke_api_key(label: str) -> str:
|
|
98
|
+
_validate_identifier(label, "label")
|
|
58
99
|
return f".apikey revoke {label}"
|
|
59
100
|
|
|
60
101
|
|
|
61
102
|
def compile_grant_access(kg: str, username: str, role: str) -> str:
|
|
103
|
+
_validate_identifier(kg, "kg")
|
|
104
|
+
_validate_identifier(username, "username")
|
|
105
|
+
_validate_identifier(role, "role")
|
|
62
106
|
return f".kg acl grant {kg} {username} {role}"
|
|
63
107
|
|
|
64
108
|
|
|
65
109
|
def compile_revoke_access(kg: str, username: str) -> str:
|
|
110
|
+
_validate_identifier(kg, "kg")
|
|
111
|
+
_validate_identifier(username, "username")
|
|
66
112
|
return f".kg acl revoke {kg} {username}"
|
|
67
113
|
|
|
68
114
|
|
|
69
115
|
def compile_list_acl(kg: str) -> str:
|
|
116
|
+
_validate_identifier(kg, "kg")
|
|
70
117
|
return f".kg acl list {kg}"
|